in packages/pyright-internal/src/analyzer/typeEvaluator.ts [4900:5233]
function applyDescriptorAccessMethod(
type: Type,
memberInfo: ClassMember | undefined,
baseTypeClass: ClassType,
bindToType: ClassType | TypeVarType | undefined,
isAccessedThroughObject: boolean,
flags: MemberAccessFlags,
errorNode: ExpressionNode,
memberName: string,
usage: EvaluatorUsage,
diag: DiagnosticAddendum | undefined
): DescriptorTypeResult | undefined {
const treatConstructorAsClassMember = (flags & MemberAccessFlags.TreatConstructorAsClassMethod) !== 0;
let isTypeValid = true;
let isAsymmetricDescriptor = false;
type = mapSubtypes(type, (subtype) => {
const concreteSubtype = makeTopLevelTypeVarsConcrete(subtype);
if (isClass(concreteSubtype)) {
// If it's an object, use its class to lookup the descriptor. If it's a class,
// use its metaclass instead.
let lookupClass: ClassType | undefined = concreteSubtype;
let isAccessedThroughMetaclass = false;
if (TypeBase.isInstantiable(concreteSubtype)) {
if (
concreteSubtype.details.effectiveMetaclass &&
isInstantiableClass(concreteSubtype.details.effectiveMetaclass)
) {
// When accessing a class member that is a class whose metaclass implements
// a descriptor protocol, only 'get' operations are allowed. If it's accessed
// through the object, all access methods are supported.
if (isAccessedThroughObject || usage.method === 'get') {
lookupClass = convertToInstance(concreteSubtype.details.effectiveMetaclass) as ClassType;
isAccessedThroughMetaclass = true;
} else {
lookupClass = undefined;
}
} else {
lookupClass = undefined;
}
}
if (lookupClass) {
let accessMethodName: string;
if (usage.method === 'get') {
accessMethodName = '__get__';
} else if (usage.method === 'set') {
accessMethodName = '__set__';
} else {
accessMethodName = '__delete__';
}
const accessMethod = lookUpClassMember(
lookupClass,
accessMethodName,
ClassMemberLookupFlags.SkipInstanceVariables
);
// Handle properties specially.
if (ClassType.isPropertyClass(lookupClass)) {
if (usage.method === 'set') {
if (!accessMethod) {
if (diag) {
diag.addMessage(
Localizer.DiagnosticAddendum.propertyMissingSetter().format({
name: memberName,
})
);
}
isTypeValid = false;
return undefined;
}
} else if (usage.method === 'del') {
if (!accessMethod) {
if (diag) {
diag.addMessage(
Localizer.DiagnosticAddendum.propertyMissingDeleter().format({
name: memberName,
})
);
}
isTypeValid = false;
return undefined;
}
}
}
if (accessMethod) {
let accessMethodType = getTypeOfMember(accessMethod);
const argList: FunctionArgument[] = [
{
// Provide "obj" argument.
argumentCategory: ArgumentCategory.Simple,
type: ClassType.isClassProperty(lookupClass)
? baseTypeClass
: isAccessedThroughObject
? bindToType || ClassType.cloneAsInstance(baseTypeClass)
: NoneType.createInstance(),
},
];
if (usage.method === 'get') {
// Provide "objtype" argument.
argList.push({
argumentCategory: ArgumentCategory.Simple,
type: baseTypeClass,
});
} else if (usage.method === 'set') {
// Provide "value" argument.
argList.push({
argumentCategory: ArgumentCategory.Simple,
type: usage.setType || UnknownType.create(),
});
}
if (
ClassType.isPropertyClass(lookupClass) &&
memberInfo &&
isInstantiableClass(memberInfo!.classType)
) {
// This specialization is required specifically for properties, which should be
// generic but are not defined that way. Because of this, we use type variables
// in the synthesized methods (e.g. __get__) for the property class that are
// defined in the class that declares the fget method.
// Infer return types before specializing. Otherwise a generic inferred
// return type won't be properly specialized.
inferReturnTypeIfNecessary(accessMethodType);
accessMethodType = partiallySpecializeType(accessMethodType, memberInfo.classType);
// If the property is being accessed from a protocol class (not an instance),
// flag this as an error because a property within a protocol is meant to be
// interpreted as a read-only attribute rather than a protocol, so accessing
// it directly from the class has an ambiguous meaning.
if (
(flags & MemberAccessFlags.AccessClassMembersOnly) !== 0 &&
ClassType.isProtocolClass(baseTypeClass)
) {
if (diag) {
diag.addMessage(Localizer.DiagnosticAddendum.propertyAccessFromProtocolClass());
}
isTypeValid = false;
}
}
if (
accessMethodType &&
(isFunction(accessMethodType) || isOverloadedFunction(accessMethodType))
) {
const methodType = accessMethodType;
// Don't emit separate diagnostics for these method calls because
// they will be redundant.
const returnType = suppressDiagnostics(errorNode, () => {
// Bind the accessor to the base object type.
let bindToClass: ClassType | undefined;
// The "bind-to" class depends on whether the descriptor is defined
// on the metaclass or the class. We handle properties specially here
// because of the way we model the __get__ logic in the property class.
if (ClassType.isPropertyClass(concreteSubtype) && !isAccessedThroughMetaclass) {
if (memberInfo && isInstantiableClass(memberInfo.classType)) {
bindToClass = memberInfo.classType;
}
} else {
if (isInstantiableClass(accessMethod.classType)) {
bindToClass = accessMethod.classType;
}
}
const boundMethodType = bindFunctionToClassOrObject(
lookupClass,
methodType,
bindToClass,
errorNode,
/* recursionCount */ undefined,
/* treatConstructorAsClassMember */ undefined,
isAccessedThroughMetaclass ? concreteSubtype : undefined
);
if (
boundMethodType &&
(isFunction(boundMethodType) || isOverloadedFunction(boundMethodType))
) {
const typeVarMap = new TypeVarMap(getTypeVarScopeId(boundMethodType));
if (bindToClass) {
typeVarMap.addSolveForScope(getTypeVarScopeId(bindToClass));
}
const callResult = validateCallArguments(
errorNode,
argList,
boundMethodType,
typeVarMap,
/* skipUnknownArgCheck */ true
);
if (callResult.argumentErrors) {
isTypeValid = false;
return AnyType.create();
}
// For set or delete, always return Any.
return usage.method === 'get'
? callResult.returnType || UnknownType.create()
: AnyType.create();
}
return undefined;
});
// Determine if we're calling __set__ on an asymmetric descriptor or property.
if (usage.method === 'set' && isClass(accessMethod.classType)) {
if (isAsymmetricDescriptorClass(accessMethod.classType)) {
isAsymmetricDescriptor = true;
}
}
if (returnType) {
return returnType;
}
}
}
}
} else if (isFunction(concreteSubtype) || isOverloadedFunction(concreteSubtype)) {
// If this function is an instance member (e.g. a lambda that was
// assigned to an instance variable), don't perform any binding.
if (!isAccessedThroughObject || (memberInfo && !memberInfo.isInstanceMember)) {
return bindFunctionToClassOrObject(
isAccessedThroughObject ? ClassType.cloneAsInstance(baseTypeClass) : baseTypeClass,
concreteSubtype,
memberInfo && isInstantiableClass(memberInfo.classType) ? memberInfo.classType : undefined,
errorNode,
/* recursionCount */ undefined,
treatConstructorAsClassMember,
bindToType
);
}
}
if (usage.method === 'set') {
if (memberInfo?.symbol.isClassVar()) {
if (flags & MemberAccessFlags.DisallowClassVarWrites) {
if (diag) {
diag.addMessage(
Localizer.DiagnosticAddendum.memberSetClassVar().format({ name: memberName })
);
}
isTypeValid = false;
return undefined;
}
}
// Check for an attempt to overwrite a final member variable.
const finalTypeDecl = memberInfo?.symbol
.getDeclarations()
.find((decl) => isFinalVariableDeclaration(decl));
if (finalTypeDecl && !ParseTreeUtils.isNodeContainedWithin(errorNode, finalTypeDecl.node)) {
// If a Final instance variable is declared in the class body but is
// being assigned within an __init__ method, it's allowed.
const enclosingFunctionNode = ParseTreeUtils.getEnclosingFunction(errorNode);
if (!enclosingFunctionNode || enclosingFunctionNode.name.value !== '__init__') {
if (diag) {
diag.addMessage(Localizer.Diagnostic.finalReassigned().format({ name: memberName }));
}
isTypeValid = false;
return undefined;
}
}
// Check for an attempt to overwrite an instance variable that is
// read-only (e.g. in a named tuple).
if (
memberInfo?.isInstanceMember &&
isClass(memberInfo.classType) &&
ClassType.isReadOnlyInstanceVariables(memberInfo.classType)
) {
if (diag) {
diag.addMessage(Localizer.DiagnosticAddendum.readOnlyAttribute().format({ name: memberName }));
}
isTypeValid = false;
return undefined;
}
let enforceTargetType = false;
if (memberInfo && memberInfo.symbol.hasTypedDeclarations()) {
// If the member has a declared type, we will enforce it.
enforceTargetType = true;
} else {
// If the member has no declared type, we will enforce it
// if this assignment isn't within the enclosing class. If
// it is within the enclosing class, the assignment is used
// to infer the type of the member.
if (memberInfo && !memberInfo.symbol.getDeclarations().some((decl) => decl.node === errorNode)) {
enforceTargetType = true;
}
}
if (enforceTargetType) {
let effectiveType = concreteSubtype;
// If the code is patching a method (defined on the class)
// with an object-level function, strip the "self" parameter
// off the original type. This is sometimes done for test
// purposes to override standard behaviors of specific methods.
if (isAccessedThroughObject) {
if (!memberInfo!.isInstanceMember && isFunction(concreteSubtype)) {
if (
FunctionType.isClassMethod(concreteSubtype) ||
FunctionType.isInstanceMethod(concreteSubtype)
) {
effectiveType = FunctionType.clone(concreteSubtype, /* stripFirstParam */ true);
}
}
}
return effectiveType;
}
}
return subtype;
});
if (!isTypeValid) {
return undefined;
}
return { type, isAsymmetricDescriptor };
}