Skip to content

Commit

Permalink
Changed behavior of super() method call when self is annotated as…
Browse files Browse the repository at this point in the history
… a protocol class. This pattern is used for annotating mix-ins. In this case, pyright should not generate an error if the protocol's method isn't implemented. This addresses #7160. (#7168)
  • Loading branch information
erictraut committed Jan 31, 2024
1 parent 8af045b commit 7585378
Show file tree
Hide file tree
Showing 4 changed files with 34 additions and 11 deletions.
8 changes: 8 additions & 0 deletions packages/pyright-internal/src/analyzer/typeEvaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8361,12 +8361,14 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
// If the bind-to type is a protocol, don't use the effective target class.
// This pattern is used for mixins, where the mixin type is a protocol class
// that is used to decorate the "self" or "cls" parameter.
let isProtocolClass = false;
if (
bindToType &&
ClassType.isProtocolClass(bindToType) &&
effectiveTargetClass &&
!ClassType.isSameGenericClass(bindToType, effectiveTargetClass)
) {
isProtocolClass = true;
effectiveTargetClass = undefined;
}

Expand All @@ -8377,6 +8379,12 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
let resultType: Type;
if (lookupResults && isInstantiableClass(lookupResults.classType)) {
resultType = lookupResults.classType;

if (isProtocolClass) {
// If the bindToType is a protocol class, set the "include subclasses" flag
// so we don't enforce that called methods are implemented within the protocol.
resultType = ClassType.cloneIncludeSubclasses(resultType);
}
} else if (
effectiveTargetClass &&
!isAnyOrUnknown(effectiveTargetClass) &&
Expand Down
14 changes: 4 additions & 10 deletions packages/pyright-internal/src/tests/samples/super11.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,34 +13,28 @@ def method2(self) -> int:
return 1

@overload
def method3(self, x: int) -> int:
...
def method3(self, x: int) -> int: ...

@overload
def method3(self, x: str) -> str:
...
def method3(self, x: str) -> str: ...

@overload
def method4(self, x: int) -> int:
...
def method4(self, x: int) -> int: ...

@overload
def method4(self, x: str) -> str:
...
def method4(self, x: str) -> str: ...

def method4(self, x: int | str) -> int | str:
return ""


class MyMixin:
def get(self: MixinProt) -> None:
# This should generate an error because method1 isn't implemented.
m1 = super().method1()
reveal_type(m1, expected_text="int")

m2 = super().method2()

# This should generate an error because method3 isn't implemented.
m3 = super().method3(1)

m4 = super().method4(2)
15 changes: 15 additions & 0 deletions packages/pyright-internal/src/tests/samples/super12.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# This sample tests the case where a class derives from a protocol
# and calls through super() to the protocol class to a method that is
# not implemented.

from typing import Protocol


class BaseProto(Protocol):
def method1(self) -> None: ...


class ProtoImpl(BaseProto):
def method1(self) -> None:
# This should generate an error.
return super().method1()
8 changes: 7 additions & 1 deletion packages/pyright-internal/src/tests/typeEvaluator2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,13 @@ test('Super10', () => {
test('Super11', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['super11.py']);

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

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

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

test('MissingSuper1', () => {
Expand Down

0 comments on commit 7585378

Please sign in to comment.