From 5f7e2c6a854e9c0d38ba4664239bb701a2cfe0af Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 14 Jun 2025 12:04:32 +0100 Subject: [PATCH 1/2] Re-widen custom properties after narrowing --- mypy/checker.py | 18 ++++++++---------- test-data/unit/check-narrowing.test | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 0639340d30bb0..9ce53875e14d4 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -4580,7 +4580,7 @@ def check_member_assignment( self, lvalue: MemberExpr, instance_type: Type, - attribute_type: Type, + set_lvalue_type: Type, rvalue: Expression, context: Context, ) -> tuple[Type, Type, bool]: @@ -4597,23 +4597,21 @@ def check_member_assignment( if (isinstance(instance_type, FunctionLike) and instance_type.is_type_obj()) or isinstance( instance_type, TypeType ): - rvalue_type, _ = self.check_simple_assignment(attribute_type, rvalue, context) - return rvalue_type, attribute_type, True + rvalue_type, _ = self.check_simple_assignment(set_lvalue_type, rvalue, context) + return rvalue_type, set_lvalue_type, True with self.msg.filter_errors(filter_deprecated=True): get_lvalue_type = self.expr_checker.analyze_ordinary_member_access( lvalue, is_lvalue=False ) - # Special case: if the rvalue_type is a subtype of both '__get__' and '__set__' types, - # and '__get__' type is narrower than '__set__', then we invoke the binder to narrow type + # Special case: if the rvalue_type is a subtype of both '__get__' type, and + # '__get__' type is narrower than '__set__', then we invoke the binder to narrow type # by this assignment. Technically, this is not safe, but in practice this is # what a user expects. - rvalue_type, _ = self.check_simple_assignment(attribute_type, rvalue, context) - infer = is_subtype(rvalue_type, get_lvalue_type) and is_subtype( - get_lvalue_type, attribute_type - ) - return rvalue_type if infer else attribute_type, attribute_type, infer + rvalue_type, _ = self.check_simple_assignment(set_lvalue_type, rvalue, context) + rvalue_type = rvalue_type if is_subtype(rvalue_type, get_lvalue_type) else get_lvalue_type + return rvalue_type, set_lvalue_type, is_subtype(get_lvalue_type, set_lvalue_type) def check_indexed_assignment( self, lvalue: IndexExpr, rvalue: Expression, context: Context diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index 3778c5276576e..0443bc8459070 100644 --- a/test-data/unit/check-narrowing.test +++ b/test-data/unit/check-narrowing.test @@ -2593,3 +2593,22 @@ def baz(item: Base) -> None: reveal_type(item) # N: Revealed type is "__main__." item.bar() [builtins fixtures/isinstance.pyi] + +[case testCustomSetterNarrowingReWidened] +class B: ... +class C(B): ... +class C1(B): ... +class D(C): ... + +class Test: + @property + def foo(self) -> C: ... + @foo.setter + def foo(self, val: B) -> None: ... + +t: Test +t.foo = D() +reveal_type(t.foo) # N: Revealed type is "__main__.D" +t.foo = C1() +reveal_type(t.foo) # N: Revealed type is "__main__.C" +[builtins fixtures/property.pyi] From e9c58d83724adec1ead7303f1af4d4f9220d3a97 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 18 Jun 2025 20:37:58 +0100 Subject: [PATCH 2/2] Fix typo Co-authored-by: Stanislav Terliakov <50529348+sterliakov@users.noreply.github.com> --- mypy/checker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 9ce53875e14d4..02d45bf6b8404 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -4605,7 +4605,7 @@ def check_member_assignment( lvalue, is_lvalue=False ) - # Special case: if the rvalue_type is a subtype of both '__get__' type, and + # Special case: if the rvalue_type is a subtype of '__get__' type, and # '__get__' type is narrower than '__set__', then we invoke the binder to narrow type # by this assignment. Technically, this is not safe, but in practice this is # what a user expects.