Skip to content

Commit 96fcd59

Browse files
Re-widen custom properties after narrowing (#19296)
Fixes #10399 This is another smaller cleanup for #7724. The current logic as documented is IMO correct, for attributes (either properties or custom descriptors) with setter type different from getter type, we narrow the attribute type in an assignment if: * The attribute is "normalizing", i.e. getter type is a subtype of setter type (e.g. `Sequence[Employee]` is normalized to `tuple[Employee, ...]`) * The given r.h.s. type in the assignment is a subtype of getter type (and thus transitively the setter as well), e.g. `tuple[Manager, ...]` vs `tuple[Employee, ...]` in the example above. The problem was that this logic was implemented too literally, as a result assignments that didn't satisfy these two rules were simply ignored (thus making previous narrowing incorrectly "sticky"). In fact, we also need to re-widen previously narrowed types whenever second condition is not satisfied. (I also decided to rename one variable name to make it more obvious.) --------- Co-authored-by: Stanislav Terliakov <50529348+sterliakov@users.noreply.github.com>
1 parent ad57093 commit 96fcd59

File tree

2 files changed

+27
-10
lines changed

2 files changed

+27
-10
lines changed

mypy/checker.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4594,7 +4594,7 @@ def check_member_assignment(
45944594
self,
45954595
lvalue: MemberExpr,
45964596
instance_type: Type,
4597-
attribute_type: Type,
4597+
set_lvalue_type: Type,
45984598
rvalue: Expression,
45994599
context: Context,
46004600
) -> tuple[Type, Type, bool]:
@@ -4611,23 +4611,21 @@ def check_member_assignment(
46114611
if (isinstance(instance_type, FunctionLike) and instance_type.is_type_obj()) or isinstance(
46124612
instance_type, TypeType
46134613
):
4614-
rvalue_type, _ = self.check_simple_assignment(attribute_type, rvalue, context)
4615-
return rvalue_type, attribute_type, True
4614+
rvalue_type, _ = self.check_simple_assignment(set_lvalue_type, rvalue, context)
4615+
return rvalue_type, set_lvalue_type, True
46164616

46174617
with self.msg.filter_errors(filter_deprecated=True):
46184618
get_lvalue_type = self.expr_checker.analyze_ordinary_member_access(
46194619
lvalue, is_lvalue=False
46204620
)
46214621

4622-
# Special case: if the rvalue_type is a subtype of both '__get__' and '__set__' types,
4623-
# and '__get__' type is narrower than '__set__', then we invoke the binder to narrow type
4622+
# Special case: if the rvalue_type is a subtype of '__get__' type, and
4623+
# '__get__' type is narrower than '__set__', then we invoke the binder to narrow type
46244624
# by this assignment. Technically, this is not safe, but in practice this is
46254625
# what a user expects.
4626-
rvalue_type, _ = self.check_simple_assignment(attribute_type, rvalue, context)
4627-
infer = is_subtype(rvalue_type, get_lvalue_type) and is_subtype(
4628-
get_lvalue_type, attribute_type
4629-
)
4630-
return rvalue_type if infer else attribute_type, attribute_type, infer
4626+
rvalue_type, _ = self.check_simple_assignment(set_lvalue_type, rvalue, context)
4627+
rvalue_type = rvalue_type if is_subtype(rvalue_type, get_lvalue_type) else get_lvalue_type
4628+
return rvalue_type, set_lvalue_type, is_subtype(get_lvalue_type, set_lvalue_type)
46314629

46324630
def check_indexed_assignment(
46334631
self, lvalue: IndexExpr, rvalue: Expression, context: Context

test-data/unit/check-narrowing.test

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2593,3 +2593,22 @@ def baz(item: Base) -> None:
25932593
reveal_type(item) # N: Revealed type is "__main__.<subclass of "__main__.Base" and "__main__.BarMixin">"
25942594
item.bar()
25952595
[builtins fixtures/isinstance.pyi]
2596+
2597+
[case testCustomSetterNarrowingReWidened]
2598+
class B: ...
2599+
class C(B): ...
2600+
class C1(B): ...
2601+
class D(C): ...
2602+
2603+
class Test:
2604+
@property
2605+
def foo(self) -> C: ...
2606+
@foo.setter
2607+
def foo(self, val: B) -> None: ...
2608+
2609+
t: Test
2610+
t.foo = D()
2611+
reveal_type(t.foo) # N: Revealed type is "__main__.D"
2612+
t.foo = C1()
2613+
reveal_type(t.foo) # N: Revealed type is "__main__.C"
2614+
[builtins fixtures/property.pyi]

0 commit comments

Comments
 (0)