Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions packages/pyright-internal/src/analyzer/typeEvaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6843,8 +6843,8 @@ export function createTypeEvaluator(

// Determine if we're calling __set__ on an asymmetric descriptor or property.
let isAsymmetricAccessor = false;
if (usage.method === 'set' && isClass(methodClassType)) {
if (isAsymmetricDescriptorClass(methodClassType)) {
if (usage.method === 'set') {
if (isAsymmetricDescriptorClass(concreteMemberType)) {
isAsymmetricAccessor = true;
}
}
Expand Down Expand Up @@ -7022,8 +7022,10 @@ export function createTypeEvaluator(

let isAsymmetric = false;

const getterSymbolResult = lookUpClassMember(classType, '__get__', MemberAccessFlags.SkipBaseClasses);
const setterSymbolResult = lookUpClassMember(classType, '__set__', MemberAccessFlags.SkipBaseClasses);
// Accessors can be defined at different levels of the descriptor's MRO.
// Compare the effective inherited getter and setter types.
const getterSymbolResult = lookUpClassMember(classType, '__get__');
const setterSymbolResult = lookUpClassMember(classType, '__set__');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning · Non-blocking recommendation

This removes SkipBaseClasses, but the lookup is still anchored to methodClassType (the class that declares __set__), rather than the concrete descriptor class. That handles the new linear case, but sibling-mixin/diamond descriptors and getter overrides below the setter can still evaluate asymmetry against the wrong accessor. If the intended behavior is to compare the effective accessors of the concrete descriptor, please thread that class into this check. [verified]


if (!getterSymbolResult || !setterSymbolResult) {
isAsymmetric = false;
Expand Down
64 changes: 64 additions & 0 deletions packages/pyright-internal/src/tests/samples/descriptor5.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# This sample tests that assignment through an inherited asymmetric descriptor
# does not narrow subsequent reads to the setter's input type.

from typing import Any, assert_type, cast


class Getter[T]:
def __get__(self, instance: Any, owner: Any = None) -> T:
return cast(Any, None)


class Descriptor[T, U](Getter[T]):
def __set__(self, instance: Any, value: T | U) -> None:
pass


class IntFromStr(Descriptor[int, str]):
pass


class Container:
value: IntFromStr


container = Container()
container.value = "1"
assert_type(container.value, int)


class SetterBase:
def __get__(self, instance: Any, owner: Any = None) -> int | str:
return cast(Any, None)

def __set__(self, instance: Any, value: int | str) -> None:
pass


class GetterOverride(SetterBase):
def __get__(self, instance: Any, owner: Any = None) -> int:
return 0


class OverrideContainer:
value: GetterOverride


override_container = OverrideContainer()
override_container.value = "1"
assert_type(override_container.value, int)


class AsymmetricProperty:
@property
def value(self) -> int:
return 0

@value.setter
def value(self, new_value: str) -> None:
pass


prop = AsymmetricProperty()
prop.value = "1"
assert_type(prop.value, int)
6 changes: 6 additions & 0 deletions packages/pyright-internal/src/tests/typeEvaluator8.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -822,6 +822,12 @@ test('Descriptor4', () => {
TestUtils.validateResults(analysisResults, 0);
});

test('Descriptor5', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['descriptor5.py']);

TestUtils.validateResults(analysisResults, 0);
});

test('Partial1', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['partial1.py']);

Expand Down
Loading