Added support for "naked" ClassVar used to annotate a class variable that is assigned a descriptor object but is later set through an object member access. This previously resulted in a false positive error. This addresses https://github.com/microsoft/pyright/issues/4838.

This commit is contained in:
Eric Traut 2023-03-24 11:10:59 -06:00
parent f63069227c
commit 8a93373e6b
3 changed files with 74 additions and 1 deletions

View File

@ -264,6 +264,7 @@ import {
getTypeVarScopeId,
getUnionSubtypeCount,
InferenceContext,
isDescriptorInstance,
isEffectivelyInstantiable,
isEllipsisType,
isIncompleteUnknown,
@ -5444,10 +5445,30 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
isInstantiableClass(containingClassType) &&
ClassType.isSameGenericClass(containingClassType, classType)
) {
type = getDeclaredTypeOfSymbol(memberInfo.symbol)?.type ?? UnknownType.create();
type = getDeclaredTypeOfSymbol(memberInfo.symbol)?.type;
if (type && isInstantiableClass(memberInfo.classType)) {
type = partiallySpecializeType(type, memberInfo.classType);
}
// If we're setting a class variable via a write through an object,
// this is normally considered a type violation. But it is allowed
// if the class variable is a descriptor object. In this case, we will
// clear the flag that causes an error to be generated.
if (usage.method === 'set' && memberInfo.symbol.isClassVar() && isAccessedThroughObject) {
const selfClass = bindToType || memberName === '__new__' ? undefined : classType;
const typeResult = getTypeOfMemberInternal(memberInfo, selfClass);
if (typeResult) {
if (isDescriptorInstance(typeResult.type, /* requireSetter */ true)) {
type = typeResult.type;
flags &= MemberAccessFlags.DisallowClassVarWrites;
}
}
}
if (!type) {
type = UnknownType.create();
}
}
}
}
@ -21283,6 +21304,13 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
considerDecl = false;
}
// If the symbol is explicitly marked as a ClassVar, consider only the
// declarations that assign to it from within the class body, not through
// a member access expression.
if (symbol.isClassVar() && decl.type === DeclarationType.Variable && decl.isDefinedByMemberAccess) {
considerDecl = false;
}
if (usageNode !== undefined) {
if (decl.type !== DeclarationType.Alias) {
// Is the declaration in the same execution scope as the "usageNode" node?

View File

@ -0,0 +1,40 @@
# This sample tests the case where a member access is performed through
# an object using a field that is annotated as a ClassVar. Normally this
# is disallowed, but it is permitted if the type of the ClassVar is
# a descriptor object.
from typing import ClassVar, Generic, TypeVar, overload, Self
T = TypeVar("T")
class Descriptor(Generic[T]):
@overload
def __get__(self, instance: None, owner) -> Self:
...
@overload
def __get__(self, instance: object, owner) -> T:
...
def __get__(self, instance: object | None, owner) -> Self | T:
...
def __set__(self, instance: object, value: T) -> None:
...
def is_null(self) -> bool:
...
class Example:
field1: ClassVar = Descriptor[str]()
field2: ClassVar = ""
def reset(self) -> None:
self.field1 = ""
# This should generate an error because field2 isn't
# a descriptor object.
self.field2 = ""

View File

@ -513,6 +513,11 @@ test('MemberAccess20', () => {
TestUtils.validateResults(analysisResults, 1);
});
test('MemberAccess21', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['memberAccess21.py']);
TestUtils.validateResults(analysisResults, 1);
});
test('DataClass1', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['dataclass1.py']);