diff --git a/packages/pyright-internal/src/analyzer/typeEvaluator.ts b/packages/pyright-internal/src/analyzer/typeEvaluator.ts index 7f759ee2af06..8029c3dc7b7c 100644 --- a/packages/pyright-internal/src/analyzer/typeEvaluator.ts +++ b/packages/pyright-internal/src/analyzer/typeEvaluator.ts @@ -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; } } @@ -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__'); if (!getterSymbolResult || !setterSymbolResult) { isAsymmetric = false; diff --git a/packages/pyright-internal/src/tests/samples/descriptor5.py b/packages/pyright-internal/src/tests/samples/descriptor5.py new file mode 100644 index 000000000000..8f06c65daa16 --- /dev/null +++ b/packages/pyright-internal/src/tests/samples/descriptor5.py @@ -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) diff --git a/packages/pyright-internal/src/tests/typeEvaluator8.test.ts b/packages/pyright-internal/src/tests/typeEvaluator8.test.ts index 2af5098a6d43..554df35cc145 100644 --- a/packages/pyright-internal/src/tests/typeEvaluator8.test.ts +++ b/packages/pyright-internal/src/tests/typeEvaluator8.test.ts @@ -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']);