Skip to content
Merged
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
95 changes: 91 additions & 4 deletions packages/pyright-internal/src/analyzer/typeEvaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ import {
} from './typeEvaluatorTypes';
import { enumerateLiteralsForType } from './typeGuards';
import * as TypePrinter from './typePrinter';
import { TypeWalker } from './typeWalker';
import {
AnyType,
ClassType,
Expand Down Expand Up @@ -6949,7 +6950,12 @@ export function createTypeEvaluator(
MemberAccessFlags.SkipInstanceMembers | MemberAccessFlags.SkipAttributeAccessOverride
);

if (!methodTypeResult || methodTypeResult.typeErrors) {
const deferredMethodType =
usage.method === 'get'
? getDescriptorGetMethodWithDeferredSelfSpecialization(concreteMemberType, subDiag)
: undefined;

if ((!methodTypeResult || methodTypeResult.typeErrors) && !deferredMethodType) {
// Provide special error messages for properties.
if (ClassType.isPropertyClass(concreteMemberType) && usage.method !== 'get') {
const message =
Expand All @@ -6965,10 +6971,10 @@ export function createTypeEvaluator(
return { type: memberType };
}

const methodClassType = methodTypeResult.classType;
let methodType = methodTypeResult.type;
const methodClassType = methodTypeResult?.classType ?? concreteMemberType;
let methodType = deferredMethodType ?? methodTypeResult!.type;

if (methodTypeResult.typeErrors || !methodClassType) {
if (!methodClassType) {
if (diag && subDiag) {
diag.addAddendum(subDiag);
}
Expand Down Expand Up @@ -7165,6 +7171,87 @@ export function createTypeEvaluator(
};
}

function getDescriptorGetMethodWithDeferredSelfSpecialization(
concreteMemberType: ClassType,
diag: DiagnosticAddendum | undefined
): FunctionType | OverloadedType | undefined {
const member = lookUpClassMember(concreteMemberType, '__get__');
if (!member || !isInstantiableClass(member.unspecializedClassType)) {
return undefined;
}

const unspecializedClassType = member.unspecializedClassType;
const declaringClassType = member.classType;
if (!isInstantiableClass(declaringClassType)) {
return undefined;
}
const declaringObjectType = ClassType.cloneAsInstance(declaringClassType);

const unspecializedMethodType = getEffectiveTypeOfSymbol(member.symbol);
if (!isFunctionOrOverloaded(unspecializedMethodType)) {
return undefined;
}

// A class ParamSpec used in Concatenate within the self annotation needs to be
// solved from the concrete descriptor, rather than specialized before self is bound.
const classParamSpecs = ClassType.getTypeParams(unspecializedClassType).filter(isParamSpec);
if (classParamSpecs.length === 0) {
return undefined;
}

const signatures = isFunction(unspecializedMethodType)
? [unspecializedMethodType]
: OverloadedType.getOverloads(unspecializedMethodType);

const shouldDefer = signatures.some((signature) => {
if (signature.shared.parameters.length === 0) {
return false;
}

const selfParamType = FunctionType.getParamType(signature, 0);
if (!isClassInstance(selfParamType) || !ClassType.isSameGenericClass(selfParamType, declaringObjectType)) {
return false;
}

class ConcatenateParamSpecWalker extends TypeWalker {
found = false;

override visitFunction(type: FunctionType): void {
const paramSpec = FunctionType.getParamSpecFromArgsKwargs(type);
if (
paramSpec &&
FunctionType.cloneRemoveParamSpecArgsKwargs(type).shared.parameters.length > 0 &&
classParamSpecs.some((classParamSpec) => isTypeSame(paramSpec, classParamSpec))
) {
this.found = true;
this.cancelWalk();
return;
}

super.visitFunction(type);
}
}

const walker = new ConcatenateParamSpecWalker();
selfParamType.priv.typeArgs?.forEach((typeArg) => walker.walk(typeArg));

return walker.found;
});

if (!shouldDefer) {
return undefined;
}

return bindFunctionToClassOrObject(
declaringObjectType,
unspecializedMethodType,
declaringClassType,
/* treatConstructorAsClassMethod */ false,
concreteMemberType,
diag
);
}

function bindMethodForMemberAccess(
type: Type,
concreteType: FunctionType | OverloadedType,
Expand Down
189 changes: 189 additions & 0 deletions packages/pyright-internal/src/tests/samples/descriptor5.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
from __future__ import annotations

from typing import Any, Callable, Concatenate, Generic, ParamSpec, Self, TypeVar, overload

P = ParamSpec("P")
Q = ParamSpec("Q")
R = TypeVar("R")
T = TypeVar("T")


class Base(Generic[P, R]):
def __init__(self, fn: Callable[P, R]) -> None:
self.fn = fn

@overload
def __get__(self, obj: None, objtype: type | None = None) -> Self: ...

@overload
def __get__(
self: Base[Concatenate[Any, P], R],
obj: object,
objtype: type | None = None,
) -> Callable[P, R]: ...

@overload
def __get__(self, obj: object, objtype: type | None = None) -> Callable[..., R]: ...

def __get__(self, obj: object | None, objtype: type | None = None) -> Any:
raise NotImplementedError


class WrapperFresh(Generic[P, R]):
def __init__(self, fn: Callable[P, R]) -> None:
self.fn = fn

@overload
def __get__(self, obj: None, objtype: type | None = None) -> Self: ...

@overload
def __get__(
self: WrapperFresh[Concatenate[Any, Q], T],
obj: object,
objtype: type | None = None,
) -> Callable[Q, T]: ...

@overload
def __get__(self, obj: object, objtype: type | None = None) -> Callable[..., R]: ...

def __get__(self, obj: object | None, objtype: type | None = None) -> Any:
raise NotImplementedError


class WrapperStrReceiver(Generic[P, R]):
def __init__(self, fn: Callable[P, R]) -> None:
self.fn = fn

@overload
def __get__(self, obj: None, objtype: type | None = None) -> Self: ...

@overload
def __get__(
self: WrapperStrReceiver[Concatenate[str, P], R],
obj: object,
objtype: type | None = None,
) -> Callable[P, R]: ...

@overload
def __get__(self, obj: object, objtype: type | None = None) -> Callable[..., R]: ...

def __get__(self, obj: object | None, objtype: type | None = None) -> Any:
raise NotImplementedError


class Child(Base[P, R], Generic[P, R]):
def child_only(self) -> int:
return 1


class NestedWrapper(Generic[P, R]):
@overload
def __get__(self, obj: None, objtype: type | None = None) -> Self: ...

@overload
def __get__(
self: NestedWrapper[Any, list[Callable[Concatenate[Any, P], R]]],
obj: object,
objtype: type | None = None,
) -> Callable[P, R]: ...

@overload
def __get__(self, obj: object, objtype: type | None = None) -> Callable[..., Any]: ...

def __get__(self, obj: object | None, objtype: type | None = None) -> Any:
raise NotImplementedError


class SingleSignatureWrapper(Generic[P, R]):
def __get__(
self: SingleSignatureWrapper[Concatenate[Any, P], R],
obj: object,
objtype: type | None = None,
) -> Callable[P, R]:
raise NotImplementedError


def wrap_reuse(fn: Callable[P, R]) -> Base[P, R]:
return Base(fn)


def wrap_child(fn: Callable[P, R]) -> Child[P, R]:
return Child(fn)


def wrap_fresh(fn: Callable[P, R]) -> WrapperFresh[P, R]:
return WrapperFresh(fn)


def wrap_str_receiver(fn: Callable[P, R]) -> WrapperStrReceiver[P, R]:
return WrapperStrReceiver(fn)


def wrap_nested(fn: Callable[P, R]) -> NestedWrapper[P, list[Callable[P, R]]]:
return NestedWrapper()


def wrap_single(fn: Callable[P, R]) -> SingleSignatureWrapper[P, R]:
return SingleSignatureWrapper()


class A:
@wrap_reuse
def reuse(self, x: int) -> bool:
return True

@wrap_fresh
def fresh(self, x: int) -> bool:
return True

@wrap_str_receiver
def incompatible_receiver(self, x: int) -> bool:
return True

@wrap_child
def inherited(self, x: int) -> bool:
return True

@wrap_nested
def nested(self, x: int) -> bool:
return True

@wrap_single
def single(self, x: int) -> bool:
return True


reveal_type(A().reuse, expected_text="(x: int) -> bool")
reveal_type(A().fresh, expected_text="(x: int) -> bool")
reveal_type(A().incompatible_receiver, expected_text="(...) -> bool")
reveal_type(A().inherited, expected_text="(x: int) -> bool")
reveal_type(A().nested, expected_text="(x: int) -> bool")
reveal_type(A().single, expected_text="(x: int) -> bool")
reveal_type(A.inherited, expected_text="Child[(self: A, x: int), bool]")
reveal_type(A.inherited.child_only, expected_text="() -> int")


def strip_first(fn: Callable[Concatenate[Any, P], R]) -> Callable[P, R]:
raise NotImplementedError


def ordinary_callable(receiver: object, value: int) -> bool:
return True


reveal_type(strip_first(ordinary_callable), expected_text="(value: int) -> bool")


class NonDescriptor(Generic[P, R]):
@overload
def bind(self: NonDescriptor[Concatenate[Any, P], R], obj: object) -> Callable[P, R]: ...

@overload
def bind(self, obj: object) -> Callable[..., R]: ...

def bind(self, obj: object) -> Callable[..., R]:
raise NotImplementedError


non_descriptor = NonDescriptor[[object, int], bool]()
reveal_type(non_descriptor.bind(object()), expected_text="(...) -> bool")
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 @@ -826,6 +826,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