From 5aeb5ffd67bc1f7f6844aba86fce0639e81c7a30 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 6 Oct 2024 11:14:22 +0100 Subject: [PATCH 01/13] Some initial work --- mypy/checker.py | 22 ++++++++++++++++++---- mypy/checkmember.py | 10 ++++++++-- mypy/nodes.py | 5 +++++ test-data/unit/check-classes.test | 14 ++++++++++++++ 4 files changed, 45 insertions(+), 6 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 1bee348bc252..4a59e48a938e 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -641,6 +641,9 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None: # HACK: Infer the type of the property. assert isinstance(defn.items[0], Decorator) self.visit_decorator(defn.items[0]) + assert isinstance(defn.items[1], Decorator) + self.visit_decorator(defn.items[1]) + defn.items[0].var.setter_type = defn.items[1].func.type for fdef in defn.items: assert isinstance(fdef, Decorator) if defn.is_property: @@ -3146,7 +3149,7 @@ def check_assignment( ): # Ignore member access to modules instance_type = self.expr_checker.accept(lvalue.expr) rvalue_type, lvalue_type, infer_lvalue_type = self.check_member_assignment( - instance_type, lvalue_type, rvalue, context=rvalue + lvalue, instance_type, lvalue_type, rvalue, context=rvalue ) else: # Hacky special case for assigning a literal None @@ -4389,7 +4392,12 @@ def check_simple_assignment( return rvalue_type def check_member_assignment( - self, instance_type: Type, attribute_type: Type, rvalue: Expression, context: Context + self, + lvalue: MemberExpr, + instance_type: Type, + attribute_type: Type, + rvalue: Expression, + context: Context, ) -> tuple[Type, Type, bool]: """Type member assignment. @@ -4411,10 +4419,16 @@ def check_member_assignment( rvalue_type = self.check_simple_assignment(attribute_type, rvalue, context) return rvalue_type, attribute_type, True + with self.msg.filter_errors(): + get_lvalue_type = self.expr_checker.analyze_ordinary_member_access( + lvalue, is_lvalue=False + ) + use_binder = is_same_type(get_lvalue_type, attribute_type) + if not isinstance(attribute_type, Instance): # TODO: support __set__() for union types. rvalue_type = self.check_simple_assignment(attribute_type, rvalue, context) - return rvalue_type, attribute_type, True + return rvalue_type, attribute_type, use_binder mx = MemberContext( is_lvalue=False, @@ -4433,7 +4447,7 @@ def check_member_assignment( # (which allow you to override the descriptor with any value), but preserves # the type of accessing the attribute (even after the override). rvalue_type = self.check_simple_assignment(get_type, rvalue, context) - return rvalue_type, get_type, True + return rvalue_type, get_type, use_binder dunder_set = attribute_type.type.get_method("__set__") if dunder_set is None: diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 9dc8d5475b1a..4b3c7aff6bd1 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -767,7 +767,10 @@ def analyze_var( # Found a member variable. original_itype = itype itype = map_instance_to_supertype(itype, var.info) - typ = var.type + if var.is_settable_property and mx.is_lvalue: + typ = var.setter_type + else: + typ = var.type if typ: if isinstance(typ, PartialType): return mx.chk.handle_partial_var_type(typ, mx.is_lvalue, var, mx.context) @@ -825,7 +828,10 @@ def analyze_var( if var.is_property: # A property cannot have an overloaded type => the cast is fine. assert isinstance(expanded_signature, CallableType) - result = expanded_signature.ret_type + if var.is_settable_property and mx.is_lvalue: + result = expanded_signature.arg_types[0] + else: + result = expanded_signature.ret_type else: result = expanded_signature else: diff --git a/mypy/nodes.py b/mypy/nodes.py index dabfb463cc95..19227766b6f2 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -993,6 +993,7 @@ class Var(SymbolNode): "_fullname", "info", "type", + "setter_type", "final_value", "is_self", "is_cls", @@ -1027,6 +1028,7 @@ def __init__(self, name: str, type: mypy.types.Type | None = None) -> None: # TODO: Should be Optional[TypeInfo] self.info = VAR_NO_INFO self.type: mypy.types.Type | None = type # Declared or inferred type, or None + self.setter_type: mypy.types.Type | None = None # Is this the first argument to an ordinary method (usually "self")? self.is_self = False # Is this the first argument to a classmethod (typically "cls")? @@ -1092,6 +1094,7 @@ def serialize(self) -> JsonDict: "name": self._name, "fullname": self._fullname, "type": None if self.type is None else self.type.serialize(), + "setter_type": None if self.setter_type is None else self.setter_type.serialize(), "flags": get_flags(self, VAR_FLAGS), } if self.final_value is not None: @@ -1103,7 +1106,9 @@ def deserialize(cls, data: JsonDict) -> Var: assert data[".class"] == "Var" name = data["name"] type = None if data["type"] is None else mypy.types.deserialize_type(data["type"]) + setter_type = None if data["setter_type"] is None else mypy.types.deserialize_type(data["setter_type"]) v = Var(name, type) + v.setter_type = setter_type v.is_ready = False # Override True default set in __init__ v._fullname = data["fullname"] set_flags(v, data["flags"]) diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 5ce80faaee18..2585296018ba 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -8170,3 +8170,17 @@ class C: def f(self) -> None: __module__ # E: Name "__module__" is not defined __qualname__ # E: Name "__qualname__" is not defined + +[case testPropertySetterType] +class A: + @property + def f(self) -> int: + return 1 + @f.setter + def f(self, x: str) -> None: + pass +a = A() +a.f = '' # OK +a.f = 1 # E: Incompatible types in assignment (expression has type "int", variable has type "str") +reveal_type(a.f) # N: Revealed type is "builtins.int" +[builtins fixtures/property.pyi] From 853431b2771f6e37673d70c4468e34d5c423b4bb Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 10 Nov 2024 22:15:21 +0000 Subject: [PATCH 02/13] Fix some tests --- mypy/checker.py | 4 ++++ mypy/fixup.py | 2 ++ 2 files changed, 6 insertions(+) diff --git a/mypy/checker.py b/mypy/checker.py index 4a59e48a938e..ce4fda5f9b21 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5132,6 +5132,10 @@ def visit_decorator_inner(self, e: Decorator, allow_empty: bool = False) -> None if not allow_empty: self.fail(message_registry.MULTIPLE_OVERLOADS_REQUIRED, e) continue + if refers_to_fullname(d, "abc.abstractmethod"): + # Normally these would be removed, except for abstract settable properties, + # where it is non-trivial to remove. + continue dec = self.expr_checker.accept(d) temp = self.temp_node(sig, context=e) fullname = None diff --git a/mypy/fixup.py b/mypy/fixup.py index f2b5bc17d32e..e97373fe573e 100644 --- a/mypy/fixup.py +++ b/mypy/fixup.py @@ -209,6 +209,8 @@ def visit_var(self, v: Var) -> None: v.info = self.current_info if v.type is not None: v.type.accept(self.type_fixer) + if v.setter_type is not None: + v.setter_type.accept(self.type_fixer) def visit_type_alias(self, a: TypeAlias) -> None: a.target.accept(self.type_fixer) From 60a6d17e92ef9dca53c286e289c3b9b86643291d Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 16 Nov 2024 13:08:12 +0000 Subject: [PATCH 03/13] Fix more tests --- mypy/checkmember.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 4b3c7aff6bd1..664665a711f3 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -769,6 +769,9 @@ def analyze_var( itype = map_instance_to_supertype(itype, var.info) if var.is_settable_property and mx.is_lvalue: typ = var.setter_type + if typ is None and var.is_ready: + # Existing synthetic properties may not set setter type. Fall back to getter. + typ = var.type else: typ = var.type if typ: @@ -828,7 +831,7 @@ def analyze_var( if var.is_property: # A property cannot have an overloaded type => the cast is fine. assert isinstance(expanded_signature, CallableType) - if var.is_settable_property and mx.is_lvalue: + if var.is_settable_property and mx.is_lvalue and var.setter_type is not None: result = expanded_signature.arg_types[0] else: result = expanded_signature.ret_type From 937f115babd3347e45075fd833a5cf6443376e67 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 17 Nov 2024 19:39:37 +0000 Subject: [PATCH 04/13] Support protocols --- mypy/errors.py | 2 +- mypy/messages.py | 54 +++++-- mypy/subtypes.py | 72 +++++++--- mypy/typeops.py | 16 ++- test-data/unit/check-protocols.test | 216 ++++++++++++++++++++++++++++ 5 files changed, 328 insertions(+), 32 deletions(-) diff --git a/mypy/errors.py b/mypy/errors.py index 1b3f485d19c0..07de49200b64 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -38,7 +38,7 @@ codes.OVERRIDE, } -allowed_duplicates: Final = ["@overload", "Got:", "Expected:"] +allowed_duplicates: Final = ["@overload", "Got:", "Expected:", "Expected setter type:"] BASE_RTD_URL: Final = "https://mypy.rtfd.io/en/stable/_refs.html#code" diff --git a/mypy/messages.py b/mypy/messages.py index d63df92c80a7..5a927f95c3cc 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -55,6 +55,7 @@ from mypy.subtypes import ( IS_CLASS_OR_STATIC, IS_CLASSVAR, + IS_EXPLICIT_SETTER, IS_SETTABLE, IS_VAR, find_member, @@ -2197,22 +2198,34 @@ def report_protocol_problems( ): type_name = format_type(subtype, self.options, module_names=True) self.note(f"Following member(s) of {type_name} have conflicts:", context, code=code) - for name, got, exp in conflict_types[:MAX_ITEMS]: + for name, got, exp, is_lvalue in conflict_types[:MAX_ITEMS]: exp = get_proper_type(exp) got = get_proper_type(got) + setter_suffix = " setter type" if is_lvalue else "" if not isinstance(exp, (CallableType, Overloaded)) or not isinstance( got, (CallableType, Overloaded) ): self.note( - "{}: expected {}, got {}".format( - name, *format_type_distinctly(exp, got, options=self.options) + "{}: expected{} {}, got {}".format( + name, + setter_suffix, + *format_type_distinctly(exp, got, options=self.options), ), context, offset=OFFSET, code=code, ) + if is_lvalue and is_subtype(got, exp, options=self.options): + self.note( + "Setter types should behave contravariantly", + context, + offset=OFFSET, + code=code, + ) else: - self.note("Expected:", context, offset=OFFSET, code=code) + self.note( + "Expected{}:".format(setter_suffix), context, offset=OFFSET, code=code + ) if isinstance(exp, CallableType): self.note( pretty_callable(exp, self.options, skip_self=class_obj or is_module), @@ -3003,12 +3016,12 @@ def get_missing_protocol_members(left: Instance, right: Instance, skip: list[str def get_conflict_protocol_types( left: Instance, right: Instance, class_obj: bool = False, options: Options | None = None -) -> list[tuple[str, Type, Type]]: +) -> list[tuple[str, Type, Type, bool]]: """Find members that are defined in 'left' but have incompatible types. - Return them as a list of ('member', 'got', 'expected'). + Return them as a list of ('member', 'got', 'expected', 'is_lvalue'). """ assert right.type.is_protocol - conflicts: list[tuple[str, Type, Type]] = [] + conflicts: list[tuple[str, Type, Type, bool]] = [] for member in right.type.protocol_members: if member in ("__init__", "__new__"): continue @@ -3018,10 +3031,31 @@ def get_conflict_protocol_types( if not subtype: continue is_compat = is_subtype(subtype, supertype, ignore_pos_arg_names=True, options=options) - if IS_SETTABLE in get_member_flags(member, right): - is_compat = is_compat and is_subtype(supertype, subtype, options=options) if not is_compat: - conflicts.append((member, subtype, supertype)) + conflicts.append((member, subtype, supertype, False)) + superflags = get_member_flags(member, right) + if IS_SETTABLE not in superflags: + continue + different_setter = False + if IS_EXPLICIT_SETTER in superflags: + set_supertype = find_member(member, right, left, is_lvalue=True) + if set_supertype != supertype: + different_setter = True + supertype = set_supertype + if IS_EXPLICIT_SETTER in get_member_flags(member, left): + set_subtype = mypy.typeops.get_protocol_member( + left, member, class_obj, is_lvalue=True + ) + if set_subtype != subtype: + different_setter = True + subtype = set_subtype + if not is_compat and not different_setter: + # We already have this conflict listed, avoid duplicates. + continue + assert supertype is not None and subtype is not None + is_compat = is_subtype(supertype, subtype, options=options) + if not is_compat: + conflicts.append((member, subtype, supertype, different_setter)) return conflicts diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 11f3421331a5..0f477c46a904 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -79,6 +79,7 @@ IS_CLASSVAR: Final = 2 IS_CLASS_OR_STATIC: Final = 3 IS_VAR: Final = 4 +IS_EXPLICIT_SETTER: Final = 5 TypeParameterChecker: _TypeAlias = Callable[[Type, Type, int, bool, "SubtypeContext"], bool] @@ -1173,7 +1174,7 @@ def f(self) -> A: ... ignore_names = member != "__call__" # __call__ can be passed kwargs # The third argument below indicates to what self type is bound. # We always bind self to the subtype. (Similarly to nominal types). - supertype = get_proper_type(find_member(member, right, left)) + supertype = find_member(member, right, left) assert supertype is not None subtype = mypy.typeops.get_protocol_member(left, member, class_obj) @@ -1182,15 +1183,6 @@ def f(self) -> A: ... # print(member, 'of', right, 'has type', supertype) if not subtype: return False - if isinstance(subtype, PartialType): - subtype = ( - NoneType() - if subtype.type is None - else Instance( - subtype.type, - [AnyType(TypeOfAny.unannotated)] * len(subtype.type.type_vars), - ) - ) if not proper_subtype: # Nominal check currently ignores arg names # NOTE: If we ever change this, be sure to also change the call to @@ -1202,15 +1194,28 @@ def f(self) -> A: ... is_compat = is_proper_subtype(subtype, supertype) if not is_compat: return False - if isinstance(subtype, NoneType) and isinstance(supertype, CallableType): + if isinstance(get_proper_type(subtype), NoneType) and isinstance( + get_proper_type(supertype), CallableType + ): # We want __hash__ = None idiom to work even without --strict-optional return False subflags = get_member_flags(member, left, class_obj=class_obj) superflags = get_member_flags(member, right) if IS_SETTABLE in superflags: # Check opposite direction for settable attributes. + if IS_EXPLICIT_SETTER in superflags: + supertype = find_member(member, right, left, is_lvalue=True) + if IS_EXPLICIT_SETTER in subflags: + subtype = mypy.typeops.get_protocol_member( + left, member, class_obj, is_lvalue=True + ) + # At this point we know attribute is present on subtype, otherwise we + # would return False above. + assert supertype is not None and subtype is not None if not is_subtype(supertype, subtype, options=options): return False + if IS_SETTABLE in superflags and IS_SETTABLE not in subflags: + return False if not class_obj: if IS_SETTABLE not in superflags: if IS_CLASSVAR in superflags and IS_CLASSVAR not in subflags: @@ -1224,8 +1229,6 @@ def f(self) -> A: ... if IS_CLASSVAR in superflags: # This can be never matched by a class object. return False - if IS_SETTABLE in superflags and IS_SETTABLE not in subflags: - return False # This rule is copied from nominal check in checker.py if IS_CLASS_OR_STATIC in superflags and IS_CLASS_OR_STATIC not in subflags: return False @@ -1244,7 +1247,13 @@ def f(self) -> A: ... def find_member( - name: str, itype: Instance, subtype: Type, is_operator: bool = False, class_obj: bool = False + name: str, + itype: Instance, + subtype: Type, + *, + is_operator: bool = False, + class_obj: bool = False, + is_lvalue: bool = False, ) -> Type | None: """Find the type of member by 'name' in 'itype's TypeInfo. @@ -1262,7 +1271,10 @@ def find_member( assert isinstance(method, OverloadedFuncDef) dec = method.items[0] assert isinstance(dec, Decorator) - return find_node_type(dec.var, itype, subtype, class_obj=class_obj) + # Pass on is_lvalue flag as this may be a property with different setter type. + return find_node_type( + dec.var, itype, subtype, class_obj=class_obj, is_lvalue=is_lvalue + ) return find_node_type(method, itype, subtype, class_obj=class_obj) else: # don't have such method, maybe variable or decorator? @@ -1327,7 +1339,10 @@ def get_member_flags(name: str, itype: Instance, class_obj: bool = False) -> set dec = method.items[0] assert isinstance(dec, Decorator) if dec.var.is_settable_property or setattr_meth: - return {IS_VAR, IS_SETTABLE} + flags = {IS_VAR, IS_SETTABLE} + if dec.var.setter_type is not None: + flags.add(IS_EXPLICIT_SETTER) + return flags else: return {IS_VAR} return set() # Just a regular method @@ -1358,7 +1373,11 @@ def get_member_flags(name: str, itype: Instance, class_obj: bool = False) -> set def find_node_type( - node: Var | FuncBase, itype: Instance, subtype: Type, class_obj: bool = False + node: Var | FuncBase, + itype: Instance, + subtype: Type, + class_obj: bool = False, + is_lvalue: bool = False, ) -> Type: """Find type of a variable or method 'node' (maybe also a decorated method). Apply type arguments from 'itype', and bind 'self' to 'subtype'. @@ -1370,7 +1389,13 @@ def find_node_type( node, fallback=Instance(itype.type.mro[-1], []) ) else: - typ = node.type + # This part and the one below are simply copies of the logic from checkmember.py. + if node.is_settable_property and is_lvalue: + typ = node.setter_type + if typ is None and node.is_ready: + typ = node.type + else: + typ = node.type if typ is not None: typ = expand_self_type(node, typ, subtype) p_typ = get_proper_type(typ) @@ -1394,7 +1419,15 @@ def find_node_type( ) if node.is_property and not class_obj: assert isinstance(signature, CallableType) - typ = signature.ret_type + if ( + isinstance(node, Var) + and node.is_settable_property + and is_lvalue + and node.setter_type is not None + ): + typ = signature.arg_types[0] + else: + typ = signature.ret_type else: typ = signature itype = map_instance_to_supertype(itype, node.info) @@ -2042,6 +2075,7 @@ def infer_variance(info: TypeInfo, i: int) -> bool: # Special case to avoid false positives (and to pass conformance tests) settable = False + # TODO: handle settable properties with setter type different from getter. typ = find_member(member, self_type, self_type) if typ: # It's okay for a method in a generic class with a contravariant type diff --git a/mypy/typeops.py b/mypy/typeops.py index 9e5e347533c6..26f580b58666 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -1140,7 +1140,9 @@ def fixup_partial_type(typ: Type) -> Type: return Instance(typ.type, [AnyType(TypeOfAny.unannotated)] * len(typ.type.type_vars)) -def get_protocol_member(left: Instance, member: str, class_obj: bool) -> ProperType | None: +def get_protocol_member( + left: Instance, member: str, class_obj: bool, is_lvalue: bool = False +) -> Type | None: if member == "__call__" and class_obj: # Special case: class objects always have __call__ that is just the constructor. from mypy.checkmember import type_object_type @@ -1157,4 +1159,14 @@ def named_type(fullname: str) -> Instance: from mypy.subtypes import find_member - return get_proper_type(find_member(member, left, left, class_obj=class_obj)) + subtype = find_member(member, left, left, class_obj=class_obj, is_lvalue=is_lvalue) + if isinstance(subtype, PartialType): + subtype = ( + NoneType() + if subtype.type is None + else Instance( + subtype.type, + [AnyType(TypeOfAny.unannotated)] * len(subtype.type.type_vars), + ) + ) + return subtype diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index 0367be3dde65..e6cfee03d350 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -4236,3 +4236,219 @@ class SupportsAdd(Protocol): x: SupportsAdd = NumpyFloat() [builtins fixtures/tuple.pyi] + +[case testSetterPropertyProtocolSubtypingBoth] +from typing import Protocol + +class B1: ... +class C1(B1): ... +class B2: ... +class C2(B2): ... + +class P1(Protocol): + @property + def foo(self) -> B1: ... + @foo.setter + def foo(self, x: C2) -> None: ... + +class P2(Protocol): + @property + def foo(self) -> B1: ... + @foo.setter + def foo(self, x: B2) -> None: ... + +class A1: + @property + def foo(self) -> B1: ... + @foo.setter + def foo(self, x: C2) -> None: ... + +class A2: + @property + def foo(self) -> C1: ... + @foo.setter + def foo(self, x: C2) -> None: ... + +class A3: + @property + def foo(self) -> C1: ... + @foo.setter + def foo(self, x: str) -> None: ... + +class A4: + @property + def foo(self) -> str: ... + @foo.setter + def foo(self, x: str) -> None: ... + +def f1(x: P1) -> None: ... +def f2(x: P2) -> None: ... + +a1: A1 +a2: A2 +a3: A3 +a4: A4 + +f1(a1) +f1(a2) +f1(a3) # E: Argument 1 to "f1" has incompatible type "A3"; expected "P1" \ + # N: Following member(s) of "A3" have conflicts: \ + # N: foo: expected setter type "C2", got "str" +f1(a4) # E: Argument 1 to "f1" has incompatible type "A4"; expected "P1" \ + # N: Following member(s) of "A4" have conflicts: \ + # N: foo: expected "B1", got "str" \ + # N: foo: expected setter type "C2", got "str" + +f2(a1) # E: Argument 1 to "f2" has incompatible type "A1"; expected "P2" \ + # N: Following member(s) of "A1" have conflicts: \ + # N: foo: expected setter type "B2", got "C2" \ + # N: Setter types should behave contravariantly +f2(a2) # E: Argument 1 to "f2" has incompatible type "A2"; expected "P2" \ + # N: Following member(s) of "A2" have conflicts: \ + # N: foo: expected setter type "B2", got "C2" \ + # N: Setter types should behave contravariantly +f2(a3) # E: Argument 1 to "f2" has incompatible type "A3"; expected "P2" \ + # N: Following member(s) of "A3" have conflicts: \ + # N: foo: expected setter type "B2", got "str" +f2(a4) # E: Argument 1 to "f2" has incompatible type "A4"; expected "P2" \ + # N: Following member(s) of "A4" have conflicts: \ + # N: foo: expected "B1", got "str" \ + # N: foo: expected setter type "B2", got "str" +[builtins fixtures/property.pyi] + +[case testSetterPropertyProtocolSubtypingVarSuper] +from typing import Protocol + +class B1: ... +class C1(B1): ... + +class P1(Protocol): + foo: B1 + +class P2(Protocol): + foo: C1 + +class A1: + @property + def foo(self) -> B1: ... + @foo.setter + def foo(self, x: C1) -> None: ... + +class A2: + @property + def foo(self) -> C1: ... + @foo.setter + def foo(self, x: B1) -> None: ... + +class A3: + @property + def foo(self) -> C1: ... + @foo.setter + def foo(self, x: str) -> None: ... + +class A4: + @property + def foo(self) -> str: ... + @foo.setter + def foo(self, x: str) -> None: ... + +def f1(x: P1) -> None: ... +def f2(x: P2) -> None: ... + +a1: A1 +a2: A2 +a3: A3 +a4: A4 + +f1(a1) # E: Argument 1 to "f1" has incompatible type "A1"; expected "P1" \ + # N: Following member(s) of "A1" have conflicts: \ + # N: foo: expected setter type "B1", got "C1" \ + # N: Setter types should behave contravariantly +f1(a2) +f1(a3) # E: Argument 1 to "f1" has incompatible type "A3"; expected "P1" \ + # N: Following member(s) of "A3" have conflicts: \ + # N: foo: expected setter type "B1", got "str" +f1(a4) # E: Argument 1 to "f1" has incompatible type "A4"; expected "P1" \ + # N: Following member(s) of "A4" have conflicts: \ + # N: foo: expected "B1", got "str" + +f2(a1) # E: Argument 1 to "f2" has incompatible type "A1"; expected "P2" \ + # N: Following member(s) of "A1" have conflicts: \ + # N: foo: expected "C1", got "B1" +f2(a2) +f2(a3) # E: Argument 1 to "f2" has incompatible type "A3"; expected "P2" \ + # N: Following member(s) of "A3" have conflicts: \ + # N: foo: expected setter type "C1", got "str" +f2(a4) # E: Argument 1 to "f2" has incompatible type "A4"; expected "P2" \ + # N: Following member(s) of "A4" have conflicts: \ + # N: foo: expected "C1", got "str" +[builtins fixtures/property.pyi] + +[case testSetterPropertyProtocolSubtypingVarSub] +from typing import Protocol + +class B1: ... +class C1(B1): ... +class B2: ... +class C2(B2): ... + +class P1(Protocol): + @property + def foo(self) -> B1: ... + @foo.setter + def foo(self, x: C2) -> None: ... + +class P2(Protocol): + @property + def foo(self) -> B1: ... + @foo.setter + def foo(self, x: C1) -> None: ... + +class A1: + foo: B1 + +class A2: + foo: B2 + +class A3: + foo: C2 + +class A4: + foo: str + +def f1(x: P1) -> None: ... +def f2(x: P2) -> None: ... + +a1: A1 +a2: A2 +a3: A3 +a4: A4 + +f1(a1) # E: Argument 1 to "f1" has incompatible type "A1"; expected "P1" \ + # N: Following member(s) of "A1" have conflicts: \ + # N: foo: expected setter type "C2", got "B1" +f1(a2) # E: Argument 1 to "f1" has incompatible type "A2"; expected "P1" \ + # N: Following member(s) of "A2" have conflicts: \ + # N: foo: expected "B1", got "B2" +f1(a3) # E: Argument 1 to "f1" has incompatible type "A3"; expected "P1" \ + # N: Following member(s) of "A3" have conflicts: \ + # N: foo: expected "B1", got "C2" +f1(a4) # E: Argument 1 to "f1" has incompatible type "A4"; expected "P1" \ + # N: Following member(s) of "A4" have conflicts: \ + # N: foo: expected "B1", got "str" \ + # N: foo: expected setter type "C2", got "str" + +f2(a1) +f2(a2) # E: Argument 1 to "f2" has incompatible type "A2"; expected "P2" \ + # N: Following member(s) of "A2" have conflicts: \ + # N: foo: expected "B1", got "B2" \ + # N: foo: expected setter type "C1", got "B2" +f2(a3) # E: Argument 1 to "f2" has incompatible type "A3"; expected "P2" \ + # N: Following member(s) of "A3" have conflicts: \ + # N: foo: expected "B1", got "C2" \ + # N: foo: expected setter type "C1", got "C2" +f2(a4) # E: Argument 1 to "f2" has incompatible type "A4"; expected "P2" \ + # N: Following member(s) of "A4" have conflicts: \ + # N: foo: expected "B1", got "str" \ + # N: foo: expected setter type "C1", got "str" +[builtins fixtures/property.pyi] From 0f65434707d0f446e77572fda9df0ff2decf4481 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 18 Jan 2025 22:56:18 +0000 Subject: [PATCH 05/13] Some incremental progress --- mypy/checker.py | 340 +++++++++++++++++------------- mypy/nodes.py | 2 +- test-data/unit/check-classes.test | 107 ++++++++++ 3 files changed, 302 insertions(+), 147 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index ce4fda5f9b21..de0764cd99bc 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -21,7 +21,7 @@ TypeVar, Union, cast, - overload, + overload, TypeGuard, ) from typing_extensions import TypeAlias as _TypeAlias @@ -2036,6 +2036,13 @@ def check_method_or_accessor_override_for_base( return None return found_base_method + def check_setter_type_override( + self, defn: OverloadedFuncDef, base_node: OverloadedFuncDef | Var, base: TypeInfo + ) -> None: + # TODO: implement actual logic. + setter_type = get_setter_type(defn) + original_setter_type = get_setter_type(base_node) + def check_method_override_for_base_with_name( self, defn: FuncDef | OverloadedFuncDef | Decorator, name: str, base: TypeInfo ) -> bool: @@ -2044,163 +2051,174 @@ def check_method_override_for_base_with_name( Return True if the supertype node was not analysed yet, and `defn` was deferred. """ base_attr = base.names.get(name) - if base_attr: - # The name of the method is defined in the base class. + if not base_attr: + return False + # The name of the method is defined in the base class. - # Point errors at the 'def' line (important for backward compatibility - # of type ignores). - if not isinstance(defn, Decorator): - context = defn - else: - context = defn.func - - # Construct the type of the overriding method. - # TODO: this logic is much less complete than similar one in checkmember.py - if isinstance(defn, (FuncDef, OverloadedFuncDef)): - typ: Type = self.function_type(defn) - override_class_or_static = defn.is_class or defn.is_static - override_class = defn.is_class - else: - assert defn.var.is_ready - assert defn.var.type is not None - typ = defn.var.type - override_class_or_static = defn.func.is_class or defn.func.is_static - override_class = defn.func.is_class - typ = get_proper_type(typ) - if isinstance(typ, FunctionLike) and not is_static(context): - typ = bind_self(typ, self.scope.active_self_type(), is_classmethod=override_class) - # Map the overridden method type to subtype context so that - # it can be checked for compatibility. - original_type = get_proper_type(base_attr.type) - original_node = base_attr.node - # `original_type` can be partial if (e.g.) it is originally an - # instance variable from an `__init__` block that becomes deferred. - if original_type is None or isinstance(original_type, PartialType): - if self.pass_num < self.last_pass: - # If there are passes left, defer this node until next pass, - # otherwise try reconstructing the method type from available information. - self.defer_node(defn, defn.info) - return True - elif isinstance(original_node, (FuncDef, OverloadedFuncDef)): - original_type = self.function_type(original_node) - elif isinstance(original_node, Decorator): - original_type = self.function_type(original_node.func) - elif isinstance(original_node, Var): - # Super type can define method as an attribute. - # See https://github.com/python/mypy/issues/10134 - - # We also check that sometimes `original_node.type` is None. - # This is the case when we use something like `__hash__ = None`. - if original_node.type is not None: - original_type = get_proper_type(original_node.type) - else: - original_type = NoneType() + # Point errors at the 'def' line (important for backward compatibility + # of type ignores). + if not isinstance(defn, Decorator): + context = defn + else: + context = defn.func + + # Construct the type of the overriding method. + # TODO: this logic is much less complete than similar one in checkmember.py + if isinstance(defn, (FuncDef, OverloadedFuncDef)): + typ: Type = self.function_type(defn) + override_class_or_static = defn.is_class or defn.is_static + override_class = defn.is_class + else: + assert defn.var.is_ready + assert defn.var.type is not None + typ = defn.var.type + override_class_or_static = defn.func.is_class or defn.func.is_static + override_class = defn.func.is_class + typ = get_proper_type(typ) + if isinstance(typ, FunctionLike) and not is_static(context): + typ = bind_self(typ, self.scope.active_self_type(), is_classmethod=override_class) + # Map the overridden method type to subtype context so that + # it can be checked for compatibility. + original_type = get_proper_type(base_attr.type) + original_node = base_attr.node + always_allow_covariant = False + if is_settable_property(defn) and ( + is_settable_property(original_node) or isinstance(original_node, Var) + ): + if is_custom_settable_property(defn) or ( + is_custom_settable_property(original_node) + ): + always_allow_covariant = True + self.check_setter_type_override(defn, original_node, base) + # `original_type` can be partial if (e.g.) it is originally an + # instance variable from an `__init__` block that becomes deferred. + if original_type is None or isinstance(original_type, PartialType): + if self.pass_num < self.last_pass: + # If there are passes left, defer this node until next pass, + # otherwise try reconstructing the method type from available information. + self.defer_node(defn, defn.info) + return True + elif isinstance(original_node, (FuncDef, OverloadedFuncDef)): + original_type = self.function_type(original_node) + elif isinstance(original_node, Decorator): + original_type = self.function_type(original_node.func) + elif isinstance(original_node, Var): + # Super type can define method as an attribute. + # See https://github.com/python/mypy/issues/10134 + + # We also check that sometimes `original_node.type` is None. + # This is the case when we use something like `__hash__ = None`. + if original_node.type is not None: + original_type = get_proper_type(original_node.type) else: - # Will always fail to typecheck below, since we know the node is a method original_type = NoneType() - if isinstance(original_node, (FuncDef, OverloadedFuncDef)): - original_class_or_static = original_node.is_class or original_node.is_static - elif isinstance(original_node, Decorator): - fdef = original_node.func - original_class_or_static = fdef.is_class or fdef.is_static else: - original_class_or_static = False # a variable can't be class or static + # Will always fail to typecheck below, since we know the node is a method + original_type = NoneType() + if isinstance(original_node, (FuncDef, OverloadedFuncDef)): + original_class_or_static = original_node.is_class or original_node.is_static + elif isinstance(original_node, Decorator): + fdef = original_node.func + original_class_or_static = fdef.is_class or fdef.is_static + else: + original_class_or_static = False # a variable can't be class or static - if isinstance(original_type, FunctionLike): - original_type = self.bind_and_map_method(base_attr, original_type, defn.info, base) - if original_node and is_property(original_node): - original_type = get_property_type(original_type) + if isinstance(original_type, FunctionLike): + original_type = self.bind_and_map_method(base_attr, original_type, defn.info, base) + if original_node and is_property(original_node): + original_type = get_property_type(original_type) - if is_property(defn): - inner: FunctionLike | None - if isinstance(typ, FunctionLike): - inner = typ - else: - inner = self.extract_callable_type(typ, context) - if inner is not None: - typ = inner - typ = get_property_type(typ) - if ( - isinstance(original_node, Var) - and not original_node.is_final - and (not original_node.is_property or original_node.is_settable_property) - and isinstance(defn, Decorator) - ): - # We only give an error where no other similar errors will be given. - if not isinstance(original_type, AnyType): - self.msg.fail( - "Cannot override writeable attribute with read-only property", - # Give an error on function line to match old behaviour. - defn.func, - code=codes.OVERRIDE, - ) - - if isinstance(original_type, AnyType) or isinstance(typ, AnyType): - pass - elif isinstance(original_type, FunctionLike) and isinstance(typ, FunctionLike): - # Check that the types are compatible. - ok = self.check_override( - typ, - original_type, - defn.name, - name, - base.name, - original_class_or_static, - override_class_or_static, - context, - ) - # Check if this override is covariant. + if is_property(defn): + inner: FunctionLike | None + if isinstance(typ, FunctionLike): + inner = typ + else: + inner = self.extract_callable_type(typ, context) + if inner is not None: + typ = inner + typ = get_property_type(typ) if ( - ok - and original_node - and codes.MUTABLE_OVERRIDE in self.options.enabled_error_codes - and self.is_writable_attribute(original_node) - and not is_subtype(original_type, typ, ignore_pos_arg_names=True) + isinstance(original_node, Var) + and not original_node.is_final + and (not original_node.is_property or original_node.is_settable_property) + and isinstance(defn, Decorator) ): - base_str, override_str = format_type_distinctly( - original_type, typ, options=self.options - ) - msg = message_registry.COVARIANT_OVERRIDE_OF_MUTABLE_ATTRIBUTE.with_additional_msg( - f' (base class "{base.name}" defined the type as {base_str},' - f" override has type {override_str})" - ) - self.fail(msg, context) - elif isinstance(original_type, UnionType) and any( - is_subtype(typ, orig_typ, ignore_pos_arg_names=True) - for orig_typ in original_type.items + # We only give an error where no other similar errors will be given. + if not isinstance(original_type, AnyType): + self.msg.fail( + "Cannot override writeable attribute with read-only property", + # Give an error on function line to match old behaviour. + defn.func, + code=codes.OVERRIDE, + ) + + if isinstance(original_type, AnyType) or isinstance(typ, AnyType): + pass + elif isinstance(original_type, FunctionLike) and isinstance(typ, FunctionLike): + # Check that the types are compatible. + ok = self.check_override( + typ, + original_type, + defn.name, + name, + base.name, + original_class_or_static, + override_class_or_static, + context, + ) + # Check if this override is covariant. + if ( + ok + and original_node + and codes.MUTABLE_OVERRIDE in self.options.enabled_error_codes + and self.is_writable_attribute(original_node) + and not always_allow_covariant + and not is_subtype(original_type, typ, ignore_pos_arg_names=True) ): - # This method is a subtype of at least one union variant. - if ( - original_node - and codes.MUTABLE_OVERRIDE in self.options.enabled_error_codes - and self.is_writable_attribute(original_node) - ): - # Covariant override of mutable attribute. - base_str, override_str = format_type_distinctly( - original_type, typ, options=self.options - ) - msg = message_registry.COVARIANT_OVERRIDE_OF_MUTABLE_ATTRIBUTE.with_additional_msg( - f' (base class "{base.name}" defined the type as {base_str},' - f" override has type {override_str})" - ) - self.fail(msg, context) - elif is_equivalent(original_type, typ): - # Assume invariance for a non-callable attribute here. Note - # that this doesn't affect read-only properties which can have - # covariant overrides. - pass - elif ( + base_str, override_str = format_type_distinctly( + original_type, typ, options=self.options + ) + msg = message_registry.COVARIANT_OVERRIDE_OF_MUTABLE_ATTRIBUTE.with_additional_msg( + f' (base class "{base.name}" defined the type as {base_str},' + f" override has type {override_str})" + ) + self.fail(msg, context) + elif isinstance(original_type, UnionType) and any( + is_subtype(typ, orig_typ, ignore_pos_arg_names=True) + for orig_typ in original_type.items + ): + # This method is a subtype of at least one union variant. + if ( original_node - and not self.is_writable_attribute(original_node) - and is_subtype(typ, original_type) + and codes.MUTABLE_OVERRIDE in self.options.enabled_error_codes + and self.is_writable_attribute(original_node) + and not always_allow_covariant ): - # If the attribute is read-only, allow covariance - pass - else: - self.msg.signature_incompatible_with_supertype( - defn.name, name, base.name, context, original=original_type, override=typ + # Covariant override of mutable attribute. + base_str, override_str = format_type_distinctly( + original_type, typ, options=self.options ) - return False + msg = message_registry.COVARIANT_OVERRIDE_OF_MUTABLE_ATTRIBUTE.with_additional_msg( + f' (base class "{base.name}" defined the type as {base_str},' + f" override has type {override_str})" + ) + self.fail(msg, context) + elif is_equivalent(original_type, typ): + # Assume invariance for a non-callable attribute here. Note + # that this doesn't affect read-only properties which can have + # covariant overrides. + pass + elif ( + original_node + and (not self.is_writable_attribute(original_node) or always_allow_covariant) + and is_subtype(typ, original_type) + ): + # If the attribute is read-only, allow covariance + pass + else: + self.msg.signature_incompatible_with_supertype( + defn.name, name, base.name, context, original=original_type, override=typ + ) def bind_and_map_method( self, sym: SymbolTableNode, typ: FunctionLike, sub_info: TypeInfo, super_info: TypeInfo @@ -2819,6 +2837,7 @@ class C(B, A[int]): ... # this is unsafe because... # TODO: use more principled logic to decide is_subtype() vs is_equivalent(). # We should rely on mutability of superclass node, not on types being Callable. + # (in particular handle settable properties with setter type different from getter). # start with the special case that Instance can be a subtype of FunctionLike call = None @@ -8644,6 +8663,35 @@ def is_property(defn: SymbolNode) -> bool: return False +def is_settable_property(defn: SymbolNode | None) -> TypeGuard[OverloadedFuncDef]: + if isinstance(defn, OverloadedFuncDef): + if defn.items and isinstance(defn.items[0], Decorator): + return defn.items[0].func.is_property + return False + + +def is_custom_settable_property(defn: SymbolNode) -> bool: + if not is_settable_property(defn): + return False + first_item = defn.items[0] + assert isinstance(first_item, Decorator) + if not first_item.var.is_settable_property: + return False + var = first_item.var + if var.setter_type is None: + return False + return not is_same_type( + get_property_type(get_proper_type(var.type)), get_setter_type(var.setter_type) + ) + + +def get_setter_type(t: ProperType) -> ProperType: + # TODO: handle deferrals. + if isinstance(t, CallableType): + return get_proper_type(t.arg_types[0]) + return t + + def get_property_type(t: ProperType) -> ProperType: if isinstance(t, CallableType): return get_proper_type(t.ret_type) diff --git a/mypy/nodes.py b/mypy/nodes.py index 19227766b6f2..47a0f13c240b 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -1028,7 +1028,7 @@ def __init__(self, name: str, type: mypy.types.Type | None = None) -> None: # TODO: Should be Optional[TypeInfo] self.info = VAR_NO_INFO self.type: mypy.types.Type | None = type # Declared or inferred type, or None - self.setter_type: mypy.types.Type | None = None + self.setter_type: mypy.types.ProperType | None = None # Is this the first argument to an ordinary method (usually "self")? self.is_self = False # Is this the first argument to a classmethod (typically "cls")? diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 2585296018ba..841c7df338ac 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -8050,6 +8050,112 @@ class Bar(Foo): def x(self, value: int) -> None: ... [builtins fixtures/property.pyi] +[case testOverridePropertyDifferentSetterBoth] +class B: ... +class C(B): ... + +class B1: + @property + def foo(self) -> str: ... + @foo.setter + def foo(self, x: C) -> None: ... +class C1(B1): + @property + def foo(self) -> str: ... + @foo.setter + def foo(self, x: B) -> None: ... + +class B2: + @property + def foo(self) -> str: ... + @foo.setter + def foo(self, x: B) -> None: ... +class C2(B2): + @property + def foo(self) -> str: ... + @foo.setter + def foo(self, x: C) -> None: ... + +class B3: + @property + def foo(self) -> C: ... + @foo.setter + def foo(self, x: C) -> None: ... +class C3(B3): + @property + def foo(self) -> C: ... + @foo.setter + def foo(self, x: B) -> None: ... + +class B4: + @property + def foo(self) -> C: ... + @foo.setter + def foo(self, x: B) -> None: ... +class C4(B4): + @property + def foo(self) -> C: ... + @foo.setter + def foo(self, x: C) -> None: ... + +class B5: + @property + def foo(self) -> str: ... + @foo.setter + def foo(self, x: B) -> None: ... +class C5(B5): + @property + def foo(self) -> C: ... + @foo.setter + def foo(self, x: C) -> None: ... + +class B6: + @property + def foo(self) -> B: ... + @foo.setter + def foo(self, x: B) -> None: ... +class C6(B6): + @property + def foo(self) -> C: ... + @foo.setter + def foo(self, x: B) -> None: ... +[builtins fixtures/property.pyi] + +[case testOverridePropertyDifferentSetterVarSuper] +class B: ... +class C(B): ... + +class B1: + foo: B +class C1(B1): + @property + def foo(self) -> B: ... + @foo.setter + def foo(self, x: C) -> None: ... + +class B1: + foo: C +class C1(B1): + @property + def foo(self) -> C: ... + @foo.setter + def foo(self, x: B) -> None: ... + +class B1: + foo: B +class C1(B1): + @property + def foo(self) -> C: ... + @foo.setter + def foo(self, x: B) -> None: ... +[builtins fixtures/property.pyi] + +[case testOverridePropertyDifferentSetterVarSub] +class B: ... +class C(B): ... + +[builtins fixtures/property.pyi] + [case testOverrideMethodProperty] class B: def foo(self) -> int: @@ -8181,6 +8287,7 @@ class A: pass a = A() a.f = '' # OK +reveal_type(a.f) # N: Revealed type is "builtins.int" a.f = 1 # E: Incompatible types in assignment (expression has type "int", variable has type "str") reveal_type(a.f) # N: Revealed type is "builtins.int" [builtins fixtures/property.pyi] From 30a4bbab31e233d97265cbbb8261934fd4065ddc Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 18 Jan 2025 23:32:10 +0000 Subject: [PATCH 06/13] Implement actual logic for override check --- mypy/checker.py | 53 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 5 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 54281184fc3e..9bfa02b8d0a2 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2046,11 +2046,44 @@ def check_method_or_accessor_override_for_base( return found_base_method def check_setter_type_override( - self, defn: OverloadedFuncDef, base_node: OverloadedFuncDef | Var, base: TypeInfo + self, defn: OverloadedFuncDef, base_attr: SymbolTableNode, base: TypeInfo ) -> None: - # TODO: implement actual logic. - setter_type = get_setter_type(defn) - original_setter_type = get_setter_type(base_node) + base_node = base_attr.node + assert isinstance(base_node, (OverloadedFuncDef, Var)) + original_type = get_raw_setter_type(base_node) + if isinstance(base_node, Var): + expanded_type = map_type_from_supertype(original_type, defn.info, base) + expanded_type = expand_self_type( + base_node, expanded_type, fill_typevars(defn.info) + ) + original_type = get_setter_type(get_proper_type(expanded_type)) + else: + original_type = self.bind_and_map_method(base_attr, original_type, defn.info, base) + original_type = get_setter_type(original_type) + + typ = get_raw_setter_type(defn) + assert isinstance(typ, CallableType) + typ = bind_self(typ, self.scope.active_self_type()) + + if not is_subtype(original_type, typ): + self.fail( + "Incompatible override of a setter type", + defn, + code=codes.OVERRIDE, + ) + base_str, override_str = format_type_distinctly( + original_type, typ, options=self.options + ) + self.note( + f' (base class "{base.name}" defined the type as {base_str},', + defn, + code = codes.OVERRIDE, + ) + self.note( + f" override has type {override_str})", + defn, + code=codes.OVERRIDE, + ) def check_method_override_for_base_with_name( self, defn: FuncDef | OverloadedFuncDef | Decorator, name: str, base: TypeInfo @@ -2098,7 +2131,7 @@ def check_method_override_for_base_with_name( is_custom_settable_property(original_node) ): always_allow_covariant = True - self.check_setter_type_override(defn, original_node, base) + self.check_setter_type_override(defn, base_attr, base) # `original_type` can be partial if (e.g.) it is originally an # instance variable from an `__init__` block that becomes deferred. if original_type is None or isinstance(original_type, PartialType): @@ -8744,6 +8777,16 @@ def is_custom_settable_property(defn: SymbolNode) -> bool: ) +def get_raw_setter_type(defn: OverloadedFuncDef | Var) -> ProperType: + if isinstance(defn, Var): + return get_proper_type(defn.type) + first_item = defn.items[0] + assert isinstance(first_item, Decorator) + var = first_item.var + assert var.setter_type is not None + return var.setter_type + + def get_setter_type(t: ProperType) -> ProperType: # TODO: handle deferrals. if isinstance(t, CallableType): From 26566c60604ce591e1fbc9f92eaa72f09f8873f2 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 19 Jan 2025 15:18:52 +0000 Subject: [PATCH 07/13] Implement other override scenario; some fixes --- mypy/checker.py | 127 ++++++++++++++++++++---------- test-data/unit/check-classes.test | 31 +++++++- 2 files changed, 113 insertions(+), 45 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 9bfa02b8d0a2..3352e4196b20 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2053,17 +2053,17 @@ def check_setter_type_override( original_type = get_raw_setter_type(base_node) if isinstance(base_node, Var): expanded_type = map_type_from_supertype(original_type, defn.info, base) - expanded_type = expand_self_type( + original_type = get_proper_type(expand_self_type( base_node, expanded_type, fill_typevars(defn.info) - ) - original_type = get_setter_type(get_proper_type(expanded_type)) + )) else: original_type = self.bind_and_map_method(base_attr, original_type, defn.info, base) - original_type = get_setter_type(original_type) + original_type = get_setter_type(original_type) typ = get_raw_setter_type(defn) assert isinstance(typ, CallableType) typ = bind_self(typ, self.scope.active_self_type()) + typ = get_setter_type(typ) if not is_subtype(original_type, typ): self.fail( @@ -2084,6 +2084,12 @@ def check_setter_type_override( defn, code=codes.OVERRIDE, ) + if is_subtype(typ, original_typ): + self.note( + " Setter types should behave contravariantly", + defn, + code=codes.OVERRIDE, + ) def check_method_override_for_base_with_name( self, defn: FuncDef | OverloadedFuncDef | Decorator, name: str, base: TypeInfo @@ -3407,17 +3413,47 @@ def check_compatibility_all_supers( continue base_type, base_node = self.lvalue_type_from_base(lvalue_node, base) + custom_setter = is_custom_settable_property(base_node) if isinstance(base_type, PartialType): base_type = None if base_type: assert base_node is not None if not self.check_compatibility_super( - lvalue, lvalue_type, rvalue, base, base_type, base_node + lvalue, lvalue_type, rvalue, base, base_type, base_node, + always_allow_covariant=custom_setter ): # Only show one error per variable; even if other # base classes are also incompatible return True + if lvalue_type and custom_setter: + base_type, _ = self.lvalue_type_from_base(lvalue_node, base, setter_type=True) + if not is_subtype(base_type, lvalue_type): + self.fail( + "Incompatible override of a setter type", + lvalue, + code=codes.OVERRIDE, + ) + base_str, override_str = format_type_distinctly( + base_type, lvalue_type, options=self.options + ) + self.note( + f' (base class "{base.name}" defined the type as {base_str},', + lvalue, + code=codes.OVERRIDE, + ) + self.note( + f" override has type {override_str})", + lvalue, + code=codes.OVERRIDE, + ) + if is_subtype(lvalue_type, base_type): + self.note( + " Setter types should behave contravariantly", + lvalue, + code=codes.OVERRIDE, + ) + return True if base is last_immediate_base: # At this point, the attribute was found to be compatible with all # immediate parents. @@ -3432,6 +3468,7 @@ def check_compatibility_super( base: TypeInfo, base_type: Type, base_node: Node, + always_allow_covariant: bool, ) -> bool: lvalue_node = lvalue.node assert isinstance(lvalue_node, Var) @@ -3491,6 +3528,7 @@ def check_compatibility_super( ok and codes.MUTABLE_OVERRIDE in self.options.enabled_error_codes and self.is_writable_attribute(base_node) + and not always_allow_covariant ): ok = self.check_subtype( base_type, @@ -3504,49 +3542,56 @@ def check_compatibility_super( return True def lvalue_type_from_base( - self, expr_node: Var, base: TypeInfo - ) -> tuple[Type | None, Node | None]: + self, expr_node: Var, base: TypeInfo, setter_type: bool = False, + ) -> tuple[Type | None, SymbolNode | None]: """For a NameExpr that is part of a class, walk all base classes and try to find the first class that defines a Type for the same name.""" expr_name = expr_node.name base_var = base.names.get(expr_name) - if base_var: - 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)) - if isinstance(base_node, Decorator): - base_node = base_node.func - base_type = base_node.type - - if base_type: - if not has_no_typevars(base_type): - self_type = self.scope.active_self_type() - assert self_type is not None, "Internal error: base lookup outside class" - if isinstance(self_type, TupleType): - instance = tuple_fallback(self_type) + if not base_var: + return None, None + 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)) + if isinstance(base_node, Decorator): + base_node = base_node.func + base_type = base_node.type + + if base_type: + if not has_no_typevars(base_type): + self_type = self.scope.active_self_type() + assert self_type is not None, "Internal error: base lookup outside class" + if isinstance(self_type, TupleType): + instance = tuple_fallback(self_type) + else: + instance = self_type + itype = map_instance_to_supertype(instance, base) + base_type = expand_type_by_instance(base_type, itype) + + base_type = get_proper_type(base_type) + if isinstance(base_type, CallableType) and isinstance(base_node, FuncDef): + # If we are a property, return the Type of the return + # value, not the Callable + if base_node.is_property: + base_type = get_proper_type(base_type.ret_type) + if isinstance(base_type, FunctionLike) and isinstance( + base_node, OverloadedFuncDef + ): + # Same for properties with setter + if base_node.is_property: + if setter_type: + assert isinstance(base_node.items[0], Decorator) + base_type = base_node.items[0].var.setter_type + assert isinstance(base_type, CallableType) + base_type = self.bind_and_map_method( + base_var, base_type, expr_node.info, base + ) + base_type = get_setter_type(base_type) else: - instance = self_type - itype = map_instance_to_supertype(instance, base) - base_type = expand_type_by_instance(base_type, itype) - - base_type = get_proper_type(base_type) - if isinstance(base_type, CallableType) and isinstance(base_node, FuncDef): - # If we are a property, return the Type of the return - # value, not the Callable - if base_node.is_property: - base_type = get_proper_type(base_type.ret_type) - if isinstance(base_type, FunctionLike) and isinstance( - base_node, OverloadedFuncDef - ): - # Same for properties with setter - if base_node.is_property: base_type = base_type.items[0].ret_type - - return base_type, base_node - - return None, None + return base_type, base_node def check_compatibility_classvar_super( self, node: Var, base: TypeInfo, base_node: Node | None diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 16fde8fb6d76..6d43f3b72b2e 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -8147,17 +8147,17 @@ class C1(B1): @foo.setter def foo(self, x: C) -> None: ... -class B1: +class B2: foo: C -class C1(B1): +class C2(2): @property def foo(self) -> C: ... @foo.setter def foo(self, x: B) -> None: ... -class B1: +class B3: foo: B -class C1(B1): +class C3(B3): @property def foo(self) -> C: ... @foo.setter @@ -8168,6 +8168,29 @@ class C1(B1): class B: ... class C(B): ... +class B1: + @property + def foo(self) -> B: ... + @foo.setter + def foo(self, x: C) -> None: ... +class C1(B1): + foo: C + +class B2: + @property + def foo(self) -> B: ... + @foo.setter + def foo(self, x: C) -> None: ... +class C2(B2): + foo: B + +class B3: + @property + def foo(self) -> C: ... + @foo.setter + def foo(self, x: B) -> None: ... +class C3(B3): + foo: C [builtins fixtures/property.pyi] [case testOverrideMethodProperty] From 96b71f5a43bc33934b766f7fef771222236b0320 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 19 Jan 2025 21:28:17 +0000 Subject: [PATCH 08/13] Some more fixes --- mypy/checker.py | 160 ++++++++++++------------------ mypy/checkmember.py | 2 +- mypy/messages.py | 28 ++++++ mypy/nodes.py | 3 +- test-data/unit/check-classes.test | 36 +++++-- 5 files changed, 122 insertions(+), 107 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 3352e4196b20..539ba8a36350 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -6,8 +6,8 @@ from collections import defaultdict from collections.abc import Iterable, Iterator, Mapping, Sequence, Set as AbstractSet from contextlib import ExitStack, contextmanager -from typing import Callable, Final, Generic, NamedTuple, Optional, TypeVar, Union, cast, overload, TypeGuard -from typing_extensions import TypeAlias as _TypeAlias +from typing import Callable, Final, Generic, NamedTuple, Optional, TypeVar, Union, cast, overload +from typing_extensions import TypeAlias as _TypeAlias, TypeGuard import mypy.checkexpr from mypy import errorcodes as codes, join, message_registry, nodes, operators @@ -648,8 +648,10 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None: assert isinstance(defn.items[0], Decorator) self.visit_decorator(defn.items[0]) assert isinstance(defn.items[1], Decorator) - self.visit_decorator(defn.items[1]) - defn.items[0].var.setter_type = defn.items[1].func.type + self.visit_func_def(defn.items[1].func) + setter_type = self.function_type(defn.items[1].func) + assert isinstance(setter_type, CallableType) + defn.items[0].var.setter_type = setter_type for fdef in defn.items: assert isinstance(fdef, Decorator) if defn.is_property: @@ -2057,39 +2059,18 @@ def check_setter_type_override( base_node, expanded_type, fill_typevars(defn.info) )) else: + assert isinstance(original_type, CallableType) original_type = self.bind_and_map_method(base_attr, original_type, defn.info, base) - original_type = get_setter_type(original_type) + assert isinstance(original_type, CallableType) + original_type = get_proper_type(original_type.arg_types[0]) typ = get_raw_setter_type(defn) assert isinstance(typ, CallableType) typ = bind_self(typ, self.scope.active_self_type()) - typ = get_setter_type(typ) + typ = get_proper_type(typ.arg_types[0]) if not is_subtype(original_type, typ): - self.fail( - "Incompatible override of a setter type", - defn, - code=codes.OVERRIDE, - ) - base_str, override_str = format_type_distinctly( - original_type, typ, options=self.options - ) - self.note( - f' (base class "{base.name}" defined the type as {base_str},', - defn, - code = codes.OVERRIDE, - ) - self.note( - f" override has type {override_str})", - defn, - code=codes.OVERRIDE, - ) - if is_subtype(typ, original_typ): - self.note( - " Setter types should behave contravariantly", - defn, - code=codes.OVERRIDE, - ) + self.msg.incompatible_setter_override(defn.items[1], typ, original_type, base) def check_method_override_for_base_with_name( self, defn: FuncDef | OverloadedFuncDef | Decorator, name: str, base: TypeInfo @@ -2273,6 +2254,7 @@ def check_method_override_for_base_with_name( self.msg.signature_incompatible_with_supertype( defn.name, name, base.name, context, original=original_type, override=typ ) + return False def bind_and_map_method( self, sym: SymbolTableNode, typ: FunctionLike, sub_info: TypeInfo, super_info: TypeInfo @@ -3427,32 +3409,15 @@ def check_compatibility_all_supers( # base classes are also incompatible return True if lvalue_type and custom_setter: - base_type, _ = self.lvalue_type_from_base(lvalue_node, base, setter_type=True) + base_type, _ = self.lvalue_type_from_base( + lvalue_node, base, setter_type=True + ) + # Setter type must be ready if the getter type is ready. + assert base_type is not None if not is_subtype(base_type, lvalue_type): - self.fail( - "Incompatible override of a setter type", - lvalue, - code=codes.OVERRIDE, + self.msg.incompatible_setter_override( + lvalue, lvalue_type, base_type, base ) - base_str, override_str = format_type_distinctly( - base_type, lvalue_type, options=self.options - ) - self.note( - f' (base class "{base.name}" defined the type as {base_str},', - lvalue, - code=codes.OVERRIDE, - ) - self.note( - f" override has type {override_str})", - lvalue, - code=codes.OVERRIDE, - ) - if is_subtype(lvalue_type, base_type): - self.note( - " Setter types should behave contravariantly", - lvalue, - code=codes.OVERRIDE, - ) return True if base is last_immediate_base: # At this point, the attribute was found to be compatible with all @@ -3559,39 +3524,41 @@ def lvalue_type_from_base( base_node = base_node.func base_type = base_node.type - if base_type: - if not has_no_typevars(base_type): - self_type = self.scope.active_self_type() - assert self_type is not None, "Internal error: base lookup outside class" - if isinstance(self_type, TupleType): - instance = tuple_fallback(self_type) + if not base_type: + return None, None + if not has_no_typevars(base_type): + self_type = self.scope.active_self_type() + assert self_type is not None, "Internal error: base lookup outside class" + if isinstance(self_type, TupleType): + instance = tuple_fallback(self_type) + else: + instance = self_type + itype = map_instance_to_supertype(instance, base) + base_type = expand_type_by_instance(base_type, itype) + + base_type = get_proper_type(base_type) + if isinstance(base_type, CallableType) and isinstance(base_node, FuncDef): + # If we are a property, return the Type of the return + # value, not the Callable + if base_node.is_property: + base_type = get_proper_type(base_type.ret_type) + if isinstance(base_type, FunctionLike) and isinstance( + base_node, OverloadedFuncDef + ): + # Same for properties with setter + if base_node.is_property: + if setter_type: + assert isinstance(base_node.items[0], Decorator) + base_type = base_node.items[0].var.setter_type + assert isinstance(base_type, CallableType) + base_type = self.bind_and_map_method( + base_var, base_type, expr_node.info, base + ) + assert isinstance(base_type, CallableType) + base_type = get_proper_type(base_type.arg_types[0]) else: - instance = self_type - itype = map_instance_to_supertype(instance, base) - base_type = expand_type_by_instance(base_type, itype) - - base_type = get_proper_type(base_type) - if isinstance(base_type, CallableType) and isinstance(base_node, FuncDef): - # If we are a property, return the Type of the return - # value, not the Callable - if base_node.is_property: - base_type = get_proper_type(base_type.ret_type) - if isinstance(base_type, FunctionLike) and isinstance( - base_node, OverloadedFuncDef - ): - # Same for properties with setter - if base_node.is_property: - if setter_type: - assert isinstance(base_node.items[0], Decorator) - base_type = base_node.items[0].var.setter_type - assert isinstance(base_type, CallableType) - base_type = self.bind_and_map_method( - base_var, base_type, expr_node.info, base - ) - base_type = get_setter_type(base_type) - else: - base_type = base_type.items[0].ret_type - return base_type, base_node + base_type = base_type.items[0].ret_type + return base_type, base_node def check_compatibility_classvar_super( self, node: Var, base: TypeInfo, base_node: Node | None @@ -8807,7 +8774,9 @@ def is_settable_property(defn: SymbolNode | None) -> TypeGuard[OverloadedFuncDef return False -def is_custom_settable_property(defn: SymbolNode) -> bool: +def is_custom_settable_property(defn: SymbolNode | None) -> bool: + if defn is None: + return False if not is_settable_property(defn): return False first_item = defn.items[0] @@ -8815,15 +8784,21 @@ def is_custom_settable_property(defn: SymbolNode) -> bool: if not first_item.var.is_settable_property: return False var = first_item.var - if var.setter_type is None: + if var.type is None or var.setter_type is None or isinstance(var.type, PartialType): + # The caller should defer in case of partial types or not ready variables. + return False + setter_type = var.setter_type.arg_types[1] + if isinstance(get_proper_type(setter_type), AnyType): return False return not is_same_type( - get_property_type(get_proper_type(var.type)), get_setter_type(var.setter_type) + get_property_type(get_proper_type(var.type)), setter_type ) def get_raw_setter_type(defn: OverloadedFuncDef | Var) -> ProperType: if isinstance(defn, Var): + # This function should not be called if the var is not ready. + assert defn.type is not None return get_proper_type(defn.type) first_item = defn.items[0] assert isinstance(first_item, Decorator) @@ -8832,13 +8807,6 @@ def get_raw_setter_type(defn: OverloadedFuncDef | Var) -> ProperType: return var.setter_type -def get_setter_type(t: ProperType) -> ProperType: - # TODO: handle deferrals. - if isinstance(t, CallableType): - return get_proper_type(t.arg_types[0]) - return t - - def get_property_type(t: ProperType) -> ProperType: if isinstance(t, CallableType): return get_proper_type(t.ret_type) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index f1c5f01a843a..78cff6b78cc8 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -777,7 +777,7 @@ def analyze_var( original_itype = itype itype = map_instance_to_supertype(itype, var.info) if var.is_settable_property and mx.is_lvalue: - typ = var.setter_type + typ: Type | None = var.setter_type if typ is None and var.is_ready: # Existing synthetic properties may not set setter type. Fall back to getter. typ = var.type diff --git a/mypy/messages.py b/mypy/messages.py index d4c8ebb2e457..d905af9bc661 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -1165,6 +1165,34 @@ def overload_signature_incompatible_with_supertype( note_template = 'Overload variants must be defined in the same order as they are in "{}"' self.note(note_template.format(supertype), context, code=codes.OVERRIDE) + def incompatible_setter_override( + self, defn: Context, typ: Type, original_type: Type, base: TypeInfo + ) -> None: + self.fail( + "Incompatible override of a setter type", + defn, + code=codes.OVERRIDE, + ) + base_str, override_str = format_type_distinctly( + original_type, typ, options=self.options + ) + self.note( + f' (base class "{base.name}" defined the type as {base_str},', + defn, + code=codes.OVERRIDE, + ) + self.note( + f" override has type {override_str})", + defn, + code=codes.OVERRIDE, + ) + if is_subtype(typ, original_type): + self.note( + " Setter types should behave contravariantly", + defn, + code=codes.OVERRIDE, + ) + def signature_incompatible_with_supertype( self, name: str, diff --git a/mypy/nodes.py b/mypy/nodes.py index c1371cfc9f87..4b5a99ea3a05 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -1012,7 +1012,7 @@ def __init__(self, name: str, type: mypy.types.Type | None = None) -> None: # TODO: Should be Optional[TypeInfo] self.info = VAR_NO_INFO self.type: mypy.types.Type | None = type # Declared or inferred type, or None - self.setter_type: mypy.types.ProperType | None = None + self.setter_type: mypy.types.CallableType | None = None # Is this the first argument to an ordinary method (usually "self")? self.is_self = False # Is this the first argument to a classmethod (typically "cls")? @@ -1092,6 +1092,7 @@ def deserialize(cls, data: JsonDict) -> Var: type = None if data["type"] is None else mypy.types.deserialize_type(data["type"]) setter_type = None if data["setter_type"] is None else mypy.types.deserialize_type(data["setter_type"]) v = Var(name, type) + assert setter_type is None or isinstance(setter_type, mypy.types.ProperType) and isinstance(setter_type, mypy.types.CallableType) v.setter_type = setter_type v.is_ready = False # Override True default set in __init__ v._fullname = data["fullname"] diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 6d43f3b72b2e..0afbea780718 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -784,7 +784,7 @@ class A: f: Callable[[str], None] class B(A): - @property # E: Covariant override of a mutable attribute (base class "A" defined the type as "Callable[[str], None]", override has type "Callable[[object], None]") + @property def f(self) -> Callable[[object], None]: pass @func.setter def f(self, x: object) -> None: pass @@ -8087,7 +8087,10 @@ class B2: class C2(B2): @property def foo(self) -> str: ... - @foo.setter + @foo.setter # E: Incompatible override of a setter type \ + # N: (base class "B2" defined the type as "B", \ + # N: override has type "C") \ + # N: Setter types should behave contravariantly def foo(self, x: C) -> None: ... class B3: @@ -8109,7 +8112,10 @@ class B4: class C4(B4): @property def foo(self) -> C: ... - @foo.setter + @foo.setter # E: Incompatible override of a setter type \ + # N: (base class "B4" defined the type as "B", \ + # N: override has type "C") \ + # N: Setter types should behave contravariantly def foo(self, x: C) -> None: ... class B5: @@ -8118,10 +8124,16 @@ class B5: @foo.setter def foo(self, x: B) -> None: ... class C5(B5): - @property + @property # E: Signature of "foo" incompatible with supertype "B5" \ + # N: Superclass: \ + # N: str \ + # N: Subclass: \ + # N: C def foo(self) -> C: ... - @foo.setter - def foo(self, x: C) -> None: ... + @foo.setter # E: Incompatible override of a setter type \ + # N: (base class "B5" defined the type as "B", \ + # N: override has type "str") + def foo(self, x: str) -> None: ... class B6: @property @@ -8144,12 +8156,15 @@ class B1: class C1(B1): @property def foo(self) -> B: ... - @foo.setter + @foo.setter # E: Incompatible override of a setter type \ + # N: (base class "B1" defined the type as "B", \ + # N: override has type "C") \ + # N: Setter types should behave contravariantly def foo(self, x: C) -> None: ... class B2: foo: C -class C2(2): +class C2(B2): @property def foo(self) -> C: ... @foo.setter @@ -8190,7 +8205,10 @@ class B3: @foo.setter def foo(self, x: B) -> None: ... class C3(B3): - foo: C + foo: C # E: Incompatible override of a setter type \ + # N: (base class "B3" defined the type as "B", \ + # N: override has type "C") \ + # N: Setter types should behave contravariantly [builtins fixtures/property.pyi] [case testOverrideMethodProperty] From 6cb2351caf71aa491e937cab30796f6e575548fe Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 19 Jan 2025 21:33:45 +0000 Subject: [PATCH 09/13] reformat --- mypy/checker.py | 33 +++++++++++++++------------------ mypy/messages.py | 26 +++++--------------------- mypy/nodes.py | 12 ++++++++++-- mypy/typeops.py | 3 +-- 4 files changed, 31 insertions(+), 43 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 539ba8a36350..c32e8ebe70d7 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2055,9 +2055,9 @@ def check_setter_type_override( original_type = get_raw_setter_type(base_node) if isinstance(base_node, Var): expanded_type = map_type_from_supertype(original_type, defn.info, base) - original_type = get_proper_type(expand_self_type( - base_node, expanded_type, fill_typevars(defn.info) - )) + original_type = get_proper_type( + expand_self_type(base_node, expanded_type, fill_typevars(defn.info)) + ) else: assert isinstance(original_type, CallableType) original_type = self.bind_and_map_method(base_attr, original_type, defn.info, base) @@ -2114,9 +2114,7 @@ def check_method_override_for_base_with_name( if is_settable_property(defn) and ( is_settable_property(original_node) or isinstance(original_node, Var) ): - if is_custom_settable_property(defn) or ( - is_custom_settable_property(original_node) - ): + if is_custom_settable_property(defn) or (is_custom_settable_property(original_node)): always_allow_covariant = True self.check_setter_type_override(defn, base_attr, base) # `original_type` can be partial if (e.g.) it is originally an @@ -3402,8 +3400,13 @@ def check_compatibility_all_supers( if base_type: assert base_node is not None if not self.check_compatibility_super( - lvalue, lvalue_type, rvalue, base, base_type, base_node, - always_allow_covariant=custom_setter + lvalue, + lvalue_type, + rvalue, + base, + base_type, + base_node, + always_allow_covariant=custom_setter, ): # Only show one error per variable; even if other # base classes are also incompatible @@ -3507,7 +3510,7 @@ def check_compatibility_super( return True def lvalue_type_from_base( - self, expr_node: Var, base: TypeInfo, setter_type: bool = False, + self, expr_node: Var, base: TypeInfo, setter_type: bool = False ) -> tuple[Type | None, SymbolNode | None]: """For a NameExpr that is part of a class, walk all base classes and try to find the first class that defines a Type for the same name.""" @@ -3542,18 +3545,14 @@ def lvalue_type_from_base( # value, not the Callable if base_node.is_property: base_type = get_proper_type(base_type.ret_type) - if isinstance(base_type, FunctionLike) and isinstance( - base_node, OverloadedFuncDef - ): + if isinstance(base_type, FunctionLike) and isinstance(base_node, OverloadedFuncDef): # Same for properties with setter if base_node.is_property: if setter_type: assert isinstance(base_node.items[0], Decorator) base_type = base_node.items[0].var.setter_type assert isinstance(base_type, CallableType) - base_type = self.bind_and_map_method( - base_var, base_type, expr_node.info, base - ) + base_type = self.bind_and_map_method(base_var, base_type, expr_node.info, base) assert isinstance(base_type, CallableType) base_type = get_proper_type(base_type.arg_types[0]) else: @@ -8790,9 +8789,7 @@ def is_custom_settable_property(defn: SymbolNode | None) -> bool: setter_type = var.setter_type.arg_types[1] if isinstance(get_proper_type(setter_type), AnyType): return False - return not is_same_type( - get_property_type(get_proper_type(var.type)), setter_type - ) + return not is_same_type(get_property_type(get_proper_type(var.type)), setter_type) def get_raw_setter_type(defn: OverloadedFuncDef | Var) -> ProperType: diff --git a/mypy/messages.py b/mypy/messages.py index d905af9bc661..a446722a9f9a 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -1168,30 +1168,16 @@ def overload_signature_incompatible_with_supertype( def incompatible_setter_override( self, defn: Context, typ: Type, original_type: Type, base: TypeInfo ) -> None: - self.fail( - "Incompatible override of a setter type", - defn, - code=codes.OVERRIDE, - ) - base_str, override_str = format_type_distinctly( - original_type, typ, options=self.options - ) + self.fail("Incompatible override of a setter type", defn, code=codes.OVERRIDE) + base_str, override_str = format_type_distinctly(original_type, typ, options=self.options) self.note( f' (base class "{base.name}" defined the type as {base_str},', defn, code=codes.OVERRIDE, ) - self.note( - f" override has type {override_str})", - defn, - code=codes.OVERRIDE, - ) + self.note(f" override has type {override_str})", defn, code=codes.OVERRIDE) if is_subtype(typ, original_type): - self.note( - " Setter types should behave contravariantly", - defn, - code=codes.OVERRIDE, - ) + self.note(" Setter types should behave contravariantly", defn, code=codes.OVERRIDE) def signature_incompatible_with_supertype( self, @@ -3075,9 +3061,7 @@ def get_conflict_protocol_types( different_setter = True supertype = set_supertype if IS_EXPLICIT_SETTER in get_member_flags(member, left): - set_subtype = mypy.typeops.get_protocol_member( - left, member, class_obj, is_lvalue=True - ) + set_subtype = mypy.typeops.get_protocol_member(left, member, class_obj, is_lvalue=True) if set_subtype != subtype: different_setter = True subtype = set_subtype diff --git a/mypy/nodes.py b/mypy/nodes.py index 4b5a99ea3a05..5367030b159a 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -1090,9 +1090,17 @@ def deserialize(cls, data: JsonDict) -> Var: assert data[".class"] == "Var" name = data["name"] type = None if data["type"] is None else mypy.types.deserialize_type(data["type"]) - setter_type = None if data["setter_type"] is None else mypy.types.deserialize_type(data["setter_type"]) + setter_type = ( + None + if data["setter_type"] is None + else mypy.types.deserialize_type(data["setter_type"]) + ) v = Var(name, type) - assert setter_type is None or isinstance(setter_type, mypy.types.ProperType) and isinstance(setter_type, mypy.types.CallableType) + assert ( + setter_type is None + or isinstance(setter_type, mypy.types.ProperType) + and isinstance(setter_type, mypy.types.CallableType) + ) v.setter_type = setter_type v.is_ready = False # Override True default set in __init__ v._fullname = data["fullname"] diff --git a/mypy/typeops.py b/mypy/typeops.py index 754e375b2214..1667e8431a17 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -1172,8 +1172,7 @@ def named_type(fullname: str) -> Instance: NoneType() if subtype.type is None else Instance( - subtype.type, - [AnyType(TypeOfAny.unannotated)] * len(subtype.type.type_vars), + subtype.type, [AnyType(TypeOfAny.unannotated)] * len(subtype.type.type_vars) ) ) return subtype From 19cab8448600f10fe95add6bfa5328242a03c991 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 19 Jan 2025 22:52:28 +0000 Subject: [PATCH 10/13] more fixes --- mypy/checker.py | 2 +- mypy/checkmember.py | 5 ++++- mypy/errors.py | 5 ++++- mypy/messages.py | 6 +++++- mypy/server/astmerge.py | 1 + 5 files changed, 15 insertions(+), 4 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index c32e8ebe70d7..9752dcfa652d 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -4503,7 +4503,7 @@ def check_member_assignment( rvalue_type = self.check_simple_assignment(attribute_type, rvalue, context) return rvalue_type, attribute_type, True - with self.msg.filter_errors(): + with self.msg.filter_errors(filter_deprecated=True): get_lvalue_type = self.expr_checker.analyze_ordinary_member_access( lvalue, is_lvalue=False ) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 78cff6b78cc8..f6b5e6be2c53 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -658,7 +658,10 @@ def analyze_descriptor_access( if isinstance(descriptor_type, UnionType): # Map the access over union types return make_simplified_union( - [analyze_descriptor_access(typ, mx) for typ in descriptor_type.items] + [ + analyze_descriptor_access(typ, mx, assignment=assignment) + for typ in descriptor_type.items + ] ) elif not isinstance(descriptor_type, Instance): return orig_descriptor_type diff --git a/mypy/errors.py b/mypy/errors.py index 2335261a68e7..f720cb04b16c 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -172,10 +172,12 @@ def __init__( *, filter_errors: bool | Callable[[str, ErrorInfo], bool] = False, save_filtered_errors: bool = False, + filter_deprecated: bool = False, ) -> None: self.errors = errors self._has_new_errors = False self._filter = filter_errors + self._filter_deprecated = filter_deprecated self._filtered: list[ErrorInfo] | None = [] if save_filtered_errors else None def __enter__(self) -> ErrorWatcher: @@ -196,7 +198,8 @@ def on_error(self, file: str, info: ErrorInfo) -> bool: ErrorWatcher further down the stack and from being recorded by Errors """ if info.code == codes.DEPRECATED: - return False + # Deprecated is not a type error, so it is handled on opt-in basis here. + return self._filter_deprecated self._has_new_errors = True if isinstance(self._filter, bool): diff --git a/mypy/messages.py b/mypy/messages.py index a446722a9f9a..8fde31451e2c 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -187,9 +187,13 @@ def filter_errors( *, filter_errors: bool | Callable[[str, ErrorInfo], bool] = True, save_filtered_errors: bool = False, + filter_deprecated: bool = False, ) -> ErrorWatcher: return ErrorWatcher( - self.errors, filter_errors=filter_errors, save_filtered_errors=save_filtered_errors + self.errors, + filter_errors=filter_errors, + save_filtered_errors=save_filtered_errors, + filter_deprecated=filter_deprecated, ) def add_errors(self, errors: list[ErrorInfo]) -> None: diff --git a/mypy/server/astmerge.py b/mypy/server/astmerge.py index 5dc254422328..bb5606758571 100644 --- a/mypy/server/astmerge.py +++ b/mypy/server/astmerge.py @@ -330,6 +330,7 @@ def visit_enum_call_expr(self, node: EnumCallExpr) -> None: def visit_var(self, node: Var) -> None: node.info = self.fixup(node.info) self.fixup_type(node.type) + self.fixup_type(node.setter_type) super().visit_var(node) def visit_type_alias(self, node: TypeAlias) -> None: From 2cb9117728daf14bef3f605d4807a8b7f4ca2164 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 21 Jan 2025 00:13:35 +0000 Subject: [PATCH 11/13] Minor cleanup --- mypy/checker.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 9752dcfa652d..040b9c521a22 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2050,6 +2050,12 @@ def check_method_or_accessor_override_for_base( def check_setter_type_override( self, defn: OverloadedFuncDef, base_attr: SymbolTableNode, base: TypeInfo ) -> None: + """Check override of a setter type of a mutable attribute. + + Currently, this should be only called when either base node or the current node + is a custom settable property (i.e. where setter type is different from getter type). + Note that this check is contravariant. + """ base_node = base_attr.node assert isinstance(base_node, (OverloadedFuncDef, Var)) original_type = get_raw_setter_type(base_node) @@ -2154,6 +2160,7 @@ def check_method_override_for_base_with_name( original_type = self.bind_and_map_method(base_attr, original_type, defn.info, base) if original_node and is_property(original_node): original_type = get_property_type(original_type) + if isinstance(original_node, Var): expanded_type = map_type_from_supertype(original_type, defn.info, base) expanded_type = expand_self_type( @@ -3557,6 +3564,7 @@ def lvalue_type_from_base( base_type = get_proper_type(base_type.arg_types[0]) else: base_type = base_type.items[0].ret_type + return base_type, base_node def check_compatibility_classvar_super( @@ -5224,10 +5232,6 @@ def visit_decorator_inner(self, e: Decorator, allow_empty: bool = False) -> None if not allow_empty: self.fail(message_registry.MULTIPLE_OVERLOADS_REQUIRED, e) continue - if refers_to_fullname(d, "abc.abstractmethod"): - # Normally these would be removed, except for abstract settable properties, - # where it is non-trivial to remove. - continue dec = self.expr_checker.accept(d) temp = self.temp_node(sig, context=d) fullname = None @@ -8800,6 +8804,7 @@ def get_raw_setter_type(defn: OverloadedFuncDef | Var) -> ProperType: first_item = defn.items[0] assert isinstance(first_item, Decorator) var = first_item.var + # TODO: handle synthetic properties here and elsewhere. assert var.setter_type is not None return var.setter_type From a2187dee1bf8825c1eb3654a6da28cf9af716d4d Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 22 Jan 2025 00:31:09 +0000 Subject: [PATCH 12/13] More clenup, fixes, and tests --- mypy/checker.py | 66 ++++++++++++++++++++++++------- mypy/messages.py | 4 +- mypy/nodes.py | 1 + test-data/unit/check-classes.test | 64 ++++++++++++++++++++++++++++++ 4 files changed, 119 insertions(+), 16 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 040b9c521a22..382a7aa6135a 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -651,6 +651,14 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None: self.visit_func_def(defn.items[1].func) setter_type = self.function_type(defn.items[1].func) assert isinstance(setter_type, CallableType) + if len(setter_type.arg_types) != 2: + self.fail("Invalid property setter signature", defn.items[1].func) + any_type = AnyType(TypeOfAny.from_error) + setter_type = setter_type.copy_modified( + arg_types=[any_type, any_type], + arg_kinds=[ARG_POS, ARG_POS], + arg_names=[None, None], + ) defn.items[0].var.setter_type = setter_type for fdef in defn.items: assert isinstance(fdef, Decorator) @@ -2058,22 +2066,29 @@ def check_setter_type_override( """ base_node = base_attr.node assert isinstance(base_node, (OverloadedFuncDef, Var)) - original_type = get_raw_setter_type(base_node) + original_type, is_original_setter = get_raw_setter_type(base_node) if isinstance(base_node, Var): expanded_type = map_type_from_supertype(original_type, defn.info, base) original_type = get_proper_type( expand_self_type(base_node, expanded_type, fill_typevars(defn.info)) ) else: + assert isinstance(original_type, ProperType) assert isinstance(original_type, CallableType) original_type = self.bind_and_map_method(base_attr, original_type, defn.info, base) assert isinstance(original_type, CallableType) - original_type = get_proper_type(original_type.arg_types[0]) + if is_original_setter: + original_type = original_type.arg_types[0] + else: + original_type = original_type.ret_type - typ = get_raw_setter_type(defn) - assert isinstance(typ, CallableType) + typ, is_setter = get_raw_setter_type(defn) + assert isinstance(typ, ProperType) and isinstance(typ, CallableType) typ = bind_self(typ, self.scope.active_self_type()) - typ = get_proper_type(typ.arg_types[0]) + if is_setter: + typ = typ.arg_types[0] + else: + typ = typ.ret_type if not is_subtype(original_type, typ): self.msg.incompatible_setter_override(defn.items[1], typ, original_type, base) @@ -3422,7 +3437,8 @@ def check_compatibility_all_supers( base_type, _ = self.lvalue_type_from_base( lvalue_node, base, setter_type=True ) - # Setter type must be ready if the getter type is ready. + # Setter type for a custom property must be ready if + # the getter type is ready. assert base_type is not None if not is_subtype(base_type, lvalue_type): self.msg.incompatible_setter_override( @@ -3519,8 +3535,14 @@ def check_compatibility_super( def lvalue_type_from_base( self, expr_node: Var, base: TypeInfo, setter_type: bool = False ) -> tuple[Type | None, SymbolNode | None]: - """For a NameExpr that is part of a class, walk all base classes and try - to find the first class that defines a Type for the same name.""" + """Find a type for a variable name in base class. + + Return the type found and the corresponding node defining the name or None + for both if the name is not defined in base or the node type is not known (yet). + The type returned is already properly mapped/bound to the subclass. + If setter_type is True, return setter types for settable properties (otherwise the + getter type is returned). + """ expr_name = expr_node.name base_var = base.names.get(expr_name) @@ -3558,7 +3580,8 @@ def lvalue_type_from_base( if setter_type: assert isinstance(base_node.items[0], Decorator) base_type = base_node.items[0].var.setter_type - assert isinstance(base_type, CallableType) + # This flag is True only for custom properties, so it is safe to assert. + assert base_type is not None base_type = self.bind_and_map_method(base_var, base_type, expr_node.info, base) assert isinstance(base_type, CallableType) base_type = get_proper_type(base_type.arg_types[0]) @@ -8778,6 +8801,11 @@ def is_settable_property(defn: SymbolNode | None) -> TypeGuard[OverloadedFuncDef def is_custom_settable_property(defn: SymbolNode | None) -> bool: + """Check if a node is a settable property with a non-trivial setter type. + + By non-trivial here we mean that it is known (i.e. definition was already type + checked), it is not Any, and it is different from the property getter type. + """ if defn is None: return False if not is_settable_property(defn): @@ -8796,17 +8824,27 @@ def is_custom_settable_property(defn: SymbolNode | None) -> bool: return not is_same_type(get_property_type(get_proper_type(var.type)), setter_type) -def get_raw_setter_type(defn: OverloadedFuncDef | Var) -> ProperType: +def get_raw_setter_type(defn: OverloadedFuncDef | Var) -> tuple[Type, bool]: + """Get an effective original setter type for a node. + + For a variable it is simply its type. For a property it is the type + of the setter method (if not None), or the getter method (used as fallback + for the plugin generated properties). + Return the type and a flag indicating that we didn't fall back to getter. + """ if isinstance(defn, Var): # This function should not be called if the var is not ready. assert defn.type is not None - return get_proper_type(defn.type) + return defn.type, True first_item = defn.items[0] assert isinstance(first_item, Decorator) var = first_item.var - # TODO: handle synthetic properties here and elsewhere. - assert var.setter_type is not None - return var.setter_type + # This function may be called on non-custom properties, so we need + # to handle the situation when it is synthetic (plugin generated). + if var.setter_type is not None: + return var.setter_type, True + assert var.type is not None + return var.type, False def get_property_type(t: ProperType) -> ProperType: diff --git a/mypy/messages.py b/mypy/messages.py index 8fde31451e2c..6c5f9af8b632 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -3061,12 +3061,12 @@ def get_conflict_protocol_types( different_setter = False if IS_EXPLICIT_SETTER in superflags: set_supertype = find_member(member, right, left, is_lvalue=True) - if set_supertype != supertype: + if set_supertype and not is_same_type(set_supertype, supertype): different_setter = True supertype = set_supertype if IS_EXPLICIT_SETTER in get_member_flags(member, left): set_subtype = mypy.typeops.get_protocol_member(left, member, class_obj, is_lvalue=True) - if set_subtype != subtype: + if set_subtype and not is_same_type(set_subtype, subtype): different_setter = True subtype = set_subtype if not is_compat and not different_setter: diff --git a/mypy/nodes.py b/mypy/nodes.py index 5367030b159a..9364805d44d4 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -1012,6 +1012,7 @@ def __init__(self, name: str, type: mypy.types.Type | None = None) -> None: # TODO: Should be Optional[TypeInfo] self.info = VAR_NO_INFO self.type: mypy.types.Type | None = type # Declared or inferred type, or None + # The setter type for settable properties. self.setter_type: mypy.types.CallableType | None = None # Is this the first argument to an ordinary method (usually "self")? self.is_self = False diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 0afbea780718..a64d8cc8eaad 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -8211,6 +8211,70 @@ class C3(B3): # N: Setter types should behave contravariantly [builtins fixtures/property.pyi] +[case testOverridePropertyInvalidSetter] +class B1: + @property + def foo(self) -> int: ... + @foo.setter + def foo(self, x: str) -> None: ... +class C1(B1): + @property + def foo(self) -> int: ... + @foo.setter + def foo(self) -> None: ... # E: Invalid property setter signature + +class B2: + @property + def foo(self) -> int: ... + @foo.setter + def foo(self) -> None: ... # E: Invalid property setter signature +class C2(B2): + @property + def foo(self) -> int: ... + @foo.setter + def foo(self, x: str) -> None: ... + +class B3: + @property + def foo(self) -> int: ... + @foo.setter + def foo(self) -> None: ... # E: Invalid property setter signature +class C3(B3): + foo: int +[builtins fixtures/property.pyi] + +[case testOverridePropertyGeneric] +from typing import TypeVar, Generic + +T = TypeVar("T") + +class B1(Generic[T]): + @property + def foo(self) -> int: ... + @foo.setter + def foo(self, x: T) -> None: ... +class C1(B1[str]): + @property + def foo(self) -> int: ... + @foo.setter # E: Incompatible override of a setter type \ + # N: (base class "B1" defined the type as "str", \ + # N: override has type "int") + def foo(self, x: int) -> None: ... + +class B2: + @property + def foo(self) -> int: ... + @foo.setter + def foo(self: T, x: T) -> None: ... +class C2(B2): + @property + def foo(self) -> int: ... + @foo.setter # E: Incompatible override of a setter type \ + # N: (base class "B2" defined the type as "C2", \ + # N: override has type "int") + def foo(self, x: int) -> None: ... +[builtins fixtures/property.pyi] + [case testOverrideMethodProperty] class B: def foo(self) -> int: From 019551bc1cfe7160d4b90755c94c56f4fe77d388 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 23 Jan 2025 23:45:33 +0000 Subject: [PATCH 13/13] Address CR --- mypy/checker.py | 27 ++++++++------- mypy/server/astdiff.py | 6 ++++ test-data/unit/check-classes.test | 30 ++++++++++++++++ test-data/unit/check-incremental.test | 26 ++++++++++++++ test-data/unit/fine-grained.test | 50 +++++++++++++++++++++++++++ test-data/unit/fixtures/property.pyi | 2 +- 6 files changed, 127 insertions(+), 14 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 382a7aa6135a..ec9f00f40e2d 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -647,19 +647,20 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None: # HACK: Infer the type of the property. assert isinstance(defn.items[0], Decorator) self.visit_decorator(defn.items[0]) - assert isinstance(defn.items[1], Decorator) - self.visit_func_def(defn.items[1].func) - setter_type = self.function_type(defn.items[1].func) - assert isinstance(setter_type, CallableType) - if len(setter_type.arg_types) != 2: - self.fail("Invalid property setter signature", defn.items[1].func) - any_type = AnyType(TypeOfAny.from_error) - setter_type = setter_type.copy_modified( - arg_types=[any_type, any_type], - arg_kinds=[ARG_POS, ARG_POS], - arg_names=[None, None], - ) - defn.items[0].var.setter_type = setter_type + if defn.items[0].var.is_settable_property: + assert isinstance(defn.items[1], Decorator) + self.visit_func_def(defn.items[1].func) + setter_type = self.function_type(defn.items[1].func) + assert isinstance(setter_type, CallableType) + if len(setter_type.arg_types) != 2: + self.fail("Invalid property setter signature", defn.items[1].func) + any_type = AnyType(TypeOfAny.from_error) + setter_type = setter_type.copy_modified( + arg_types=[any_type, any_type], + arg_kinds=[ARG_POS, ARG_POS], + arg_names=[None, None], + ) + defn.items[0].var.setter_type = setter_type for fdef in defn.items: assert isinstance(fdef, Decorator) if defn.is_property: diff --git a/mypy/server/astdiff.py b/mypy/server/astdiff.py index f91687823841..07bc6333ce88 100644 --- a/mypy/server/astdiff.py +++ b/mypy/server/astdiff.py @@ -245,6 +245,11 @@ def snapshot_definition(node: SymbolNode | None, common: SymbolSnapshot) -> Symb impl = node elif isinstance(node, OverloadedFuncDef) and node.impl: impl = node.impl.func if isinstance(node.impl, Decorator) else node.impl + setter_type = None + if isinstance(node, OverloadedFuncDef) and node.items: + first_item = node.items[0] + if isinstance(first_item, Decorator) and first_item.func.is_property: + setter_type = snapshot_optional_type(first_item.var.setter_type) is_trivial_body = impl.is_trivial_body if impl else False dataclass_transform_spec = find_dataclass_transform_spec(node) return ( @@ -258,6 +263,7 @@ def snapshot_definition(node: SymbolNode | None, common: SymbolSnapshot) -> Symb is_trivial_body, dataclass_transform_spec.serialize() if dataclass_transform_spec is not None else None, node.deprecated if isinstance(node, FuncDef) else None, + setter_type, # multi-part properties are stored as OverloadedFuncDef ) elif isinstance(node, Var): return ("Var", common, snapshot_optional_type(node.type), node.is_final) diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index a64d8cc8eaad..5135ef784051 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -8410,3 +8410,33 @@ reveal_type(a.f) # N: Revealed type is "builtins.int" a.f = 1 # E: Incompatible types in assignment (expression has type "int", variable has type "str") reveal_type(a.f) # N: Revealed type is "builtins.int" [builtins fixtures/property.pyi] + +[case testPropertySetterTypeGeneric] +from typing import TypeVar, Generic, List + +T = TypeVar("T") + +class B(Generic[T]): + @property + def foo(self) -> int: ... + @foo.setter + def foo(self, x: T) -> None: ... + +class C(B[List[T]]): ... + +a = C[str]() +a.foo = ["foo", "bar"] +reveal_type(a.foo) # N: Revealed type is "builtins.int" +a.foo = 1 # E: Incompatible types in assignment (expression has type "int", variable has type "List[str]") +reveal_type(a.foo) # N: Revealed type is "builtins.int" +[builtins fixtures/property.pyi] + +[case testPropertyDeleterNoSetterOK] +class C: + @property + def x(self) -> int: + return 0 + @x.deleter + def x(self) -> None: + pass +[builtins fixtures/property.pyi] diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 77170280ecae..cfaf921fcff4 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -6745,3 +6745,29 @@ from typing_extensions import TypeAlias IntOrStr: TypeAlias = int | str assert isinstance(1, IntOrStr) [builtins fixtures/type.pyi] + +[case testPropertySetterTypeIncremental] +import b +[file a.py] +class A: + @property + def f(self) -> int: + return 1 + @f.setter + def f(self, x: str) -> None: + pass +[file b.py] +from a import A +[file b.py.2] +from a import A +a = A() +a.f = '' # OK +reveal_type(a.f) +a.f = 1 +reveal_type(a.f) +[builtins fixtures/property.pyi] +[out] +[out2] +tmp/b.py:4: note: Revealed type is "builtins.int" +tmp/b.py:5: error: Incompatible types in assignment (expression has type "int", variable has type "str") +tmp/b.py:6: note: Revealed type is "builtins.int" diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 0f6e018fe325..6cec395a6925 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -11161,3 +11161,53 @@ main:6: error: class a.D is deprecated: use D2 instead main:7: error: class a.D is deprecated: use D2 instead b.py:1: error: class a.C is deprecated: use C2 instead b.py:2: error: class a.D is deprecated: use D2 instead + +[case testPropertySetterTypeFineGrained] +from a import A +a = A() +a.f = '' +[file a.py] +class A: + @property + def f(self) -> int: + return 1 + @f.setter + def f(self, x: str) -> None: + pass +[file a.py.2] +class A: + @property + def f(self) -> int: + return 1 + @f.setter + def f(self, x: int) -> None: + pass +[builtins fixtures/property.pyi] +[out] +== +main:3: error: Incompatible types in assignment (expression has type "str", variable has type "int") + +[case testPropertyDeleteSetterFineGrained] +from a import A +a = A() +a.f = 1 +[file a.py] +class A: + @property + def f(self) -> int: + return 1 + @f.setter + def f(self, x: int) -> None: + pass +[file a.py.2] +class A: + @property + def f(self) -> int: + return 1 + @f.deleter + def f(self) -> None: + pass +[builtins fixtures/property.pyi] +[out] +== +main:3: error: Property "f" defined in "A" is read-only diff --git a/test-data/unit/fixtures/property.pyi b/test-data/unit/fixtures/property.pyi index 667bdc02d0f5..933868ac9907 100644 --- a/test-data/unit/fixtures/property.pyi +++ b/test-data/unit/fixtures/property.pyi @@ -13,7 +13,7 @@ class function: pass property = object() # Dummy definition class classmethod: pass -class list: pass +class list(typing.Generic[_T]): pass class dict: pass class int: pass class float: pass