Improved member access logic to more faithfully match the Python interpreter's behavior when the member is assigned through a class, that member is a class itself, and that class has a metaclass that implements a descriptor protocol. It appears that the interpreter does not call through to the metaclass's __set__ method in this case, even though it does call its __get__ method when the member is accessed in the same circumstance.

Changed behavior of member accesses when the member is a class whose metaclass cannot be established (e.g. because it inherits from a class whose type is unknown). In this case, the previous logic returned an Unknown type for the member access. It now assumes that the metaclass does not implement a descriptor protocol, so it doesn't attempt to apply a descriptor access.
This commit is contained in:
Eric Traut 2021-09-16 18:35:05 -07:00
parent eca6f1a5c9
commit 209e9283ca
4 changed files with 168 additions and 134 deletions

View File

@ -5239,164 +5239,174 @@ export function createTypeEvaluator(
if (isClass(subtype)) {
// If it's an object, use its class to lookup the descriptor. If it's a class,
// use its metaclass instead.
let lookupClass = subtype;
let lookupClass: ClassType | undefined = subtype;
let isAccessedThroughMetaclass = false;
if (TypeBase.isInstantiable(subtype)) {
if (
!subtype.details.effectiveMetaclass ||
!isInstantiableClass(subtype.details.effectiveMetaclass)
) {
return UnknownType.create();
}
lookupClass = convertToInstance(subtype.details.effectiveMetaclass) as ClassType;
isAccessedThroughMetaclass = true;
}
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) {
diag.addMessage(
Localizer.DiagnosticAddendum.propertyMissingSetter().format({ name: memberName })
);
isTypeValid = false;
return undefined;
}
} else if (usage.method === 'del') {
if (!accessMethod) {
diag.addMessage(
Localizer.DiagnosticAddendum.propertyMissingDeleter().format({ name: memberName })
);
isTypeValid = false;
return undefined;
if (subtype.details.effectiveMetaclass && isInstantiableClass(subtype.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(subtype.details.effectiveMetaclass) as ClassType;
isAccessedThroughMetaclass = true;
} else {
lookupClass = undefined;
}
} else {
lookupClass = 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 (lookupClass) {
let accessMethodName: string;
if (usage.method === 'get') {
// Provide "objtype" argument.
argList.push({
argumentCategory: ArgumentCategory.Simple,
type: baseTypeClass,
});
accessMethodName = '__get__';
} else if (usage.method === 'set') {
// Provide "value" argument.
argList.push({
argumentCategory: ArgumentCategory.Simple,
type: usage.setType || UnknownType.create(),
});
accessMethodName = '__set__';
} else {
accessMethodName = '__delete__';
}
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.
const accessMethod = lookUpClassMember(
lookupClass,
accessMethodName,
ClassMemberLookupFlags.SkipInstanceVariables
);
// Infer return types before specializing. Otherwise a generic inferred
// return type won't be properly specialized.
if (isFunction(accessMethodType)) {
getFunctionEffectiveReturnType(accessMethodType);
} else if (isOverloadedFunction(accessMethodType)) {
accessMethodType.overloads.forEach((overload) => {
getFunctionEffectiveReturnType(overload);
// Handle properties specially.
if (ClassType.isPropertyClass(lookupClass)) {
if (usage.method === 'set') {
if (!accessMethod) {
diag.addMessage(
Localizer.DiagnosticAddendum.propertyMissingSetter().format({ name: memberName })
);
isTypeValid = false;
return undefined;
}
} else if (usage.method === 'del') {
if (!accessMethod) {
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(),
});
}
accessMethodType = partiallySpecializeType(accessMethodType, memberInfo.classType);
}
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.
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.
if (TypeBase.isInstantiable(subtype)) {
if (isInstantiableClass(accessMethod.classType)) {
bindToClass = accessMethod.classType;
}
} else if (memberInfo && isInstantiableClass(memberInfo.classType)) {
bindToClass = memberInfo.classType;
// Infer return types before specializing. Otherwise a generic inferred
// return type won't be properly specialized.
if (isFunction(accessMethodType)) {
getFunctionEffectiveReturnType(accessMethodType);
} else if (isOverloadedFunction(accessMethodType)) {
accessMethodType.overloads.forEach((overload) => {
getFunctionEffectiveReturnType(overload);
});
}
const boundMethodType = bindFunctionToClassOrObject(
lookupClass,
methodType,
bindToClass,
errorNode,
/* recursionCount */ undefined,
/* treatConstructorAsClassMember */ undefined,
isAccessedThroughMetaclass ? subtype : undefined
);
accessMethodType = partiallySpecializeType(accessMethodType, memberInfo.classType);
}
if (
boundMethodType &&
(isFunction(boundMethodType) || isOverloadedFunction(boundMethodType))
) {
const callResult = validateCallArguments(
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.
if (TypeBase.isInstantiable(subtype)) {
if (isInstantiableClass(accessMethod.classType)) {
bindToClass = accessMethod.classType;
}
} else if (memberInfo && isInstantiableClass(memberInfo.classType)) {
bindToClass = memberInfo.classType;
}
const boundMethodType = bindFunctionToClassOrObject(
lookupClass,
methodType,
bindToClass,
errorNode,
argList,
boundMethodType,
/* typeVarMap */ undefined,
/* skipUnknownArgCheck */ true
/* recursionCount */ undefined,
/* treatConstructorAsClassMember */ undefined,
isAccessedThroughMetaclass ? subtype : undefined
);
if (callResult.argumentErrors) {
isTypeValid = false;
return AnyType.create();
if (
boundMethodType &&
(isFunction(boundMethodType) || isOverloadedFunction(boundMethodType))
) {
const callResult = validateCallArguments(
errorNode,
argList,
boundMethodType,
/* typeVarMap */ undefined,
/* 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();
}
// For set or delete, always return Any.
return usage.method === 'get'
? callResult.returnType || UnknownType.create()
: AnyType.create();
return undefined;
});
if (returnType) {
return returnType;
}
return undefined;
});
if (returnType) {
return returnType;
}
}
}

View File

@ -24,7 +24,9 @@ class X:
t1: Literal["int"] = reveal_type(X.number_cls)
t2: Literal["int"] = reveal_type(X().number_cls)
# This should generate an error
X.number_cls = "hi"
X().number_cls = "hi"
# This should generate an error

View File

@ -0,0 +1,17 @@
# This sample tests a member access when the member is a class
# that inherits from Any.
from typing import Literal, Type
from unittest.mock import Mock
class MockProducer:
produce: Type[Mock] = Mock
t1: Literal["Type[Mock]"] = reveal_type(MockProducer.produce)
t2: Literal["Type[Mock]"] = reveal_type(MockProducer().produce)
t3: Literal["Mock"] = reveal_type(MockProducer.produce())
t3: Literal["Mock"] = reveal_type(MockProducer().produce())

View File

@ -377,7 +377,7 @@ test('MemberAccess9', () => {
test('MemberAccess10', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['memberAccess10.py']);
TestUtils.validateResults(analysisResults, 2);
TestUtils.validateResults(analysisResults, 3);
});
test('MemberAccess11', () => {
@ -390,6 +390,11 @@ test('MemberAccess12', () => {
TestUtils.validateResults(analysisResults, 0);
});
test('MemberAccess13', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['memberAccess13.py']);
TestUtils.validateResults(analysisResults, 0);
});
test('DataClass1', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['dataclass1.py']);