Fixed bug that results in inconsistent type narrowing on assignment based on whether the assignment occurs within the same statement that includes the (declared) type annotation for the variable and whether the type annotation is provided as a type comment. This addresses #7537. (#7638)

This commit is contained in:
Eric Traut 2024-04-07 23:13:02 -07:00 committed by GitHub
parent e65ac3854d
commit d16990abc1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 58 additions and 7 deletions

View File

@ -2210,6 +2210,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
classType: memberInfo.classType,
isIncomplete: !!memberInfo.isTypeIncomplete,
isAsymmetricAccessor: memberInfo.isAsymmetricAccessor,
narrowedTypeForSet: memberInfo.narrowedTypeForSet,
memberAccessDeprecationInfo: memberInfo.memberAccessDeprecationInfo,
typeErrors: memberInfo.isDescriptorError,
};
@ -3449,7 +3450,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
}
const resultToCache: TypeResult = {
type,
type: setTypeResult.narrowedTypeForSet ?? type,
isIncomplete: isTypeIncomplete,
memberAccessDeprecationInfo: setTypeResult.memberAccessDeprecationInfo,
};
@ -5195,6 +5196,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
let diag = new DiagnosticAddendum();
const fileInfo = AnalyzerNodeInfo.getFileInfo(node);
let type: Type | undefined;
let narrowedTypeForSet: Type | undefined;
let isIncomplete = !!baseTypeResult.isIncomplete;
let isAsymmetricAccessor: boolean | undefined;
const isRequired = false;
@ -5324,6 +5326,14 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
/* skipSelfCondition */ true
);
if (typeResult.narrowedTypeForSet) {
narrowedTypeForSet = addConditionToType(
typeResult.narrowedTypeForSet,
getTypeCondition(baseType),
/* skipSelfCondition */ true
);
}
if (typeResult.isIncomplete) {
isIncomplete = true;
}
@ -5565,7 +5575,15 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
reportUseOfTypeCheckOnly(type, node.memberName);
}
return { type, isIncomplete, isAsymmetricAccessor, isRequired, isNotRequired, memberAccessDeprecationInfo };
return {
type,
isIncomplete,
isAsymmetricAccessor,
narrowedTypeForSet,
isRequired,
isNotRequired,
memberAccessDeprecationInfo,
};
}
function getTypeOfClassMemberName(
@ -5615,6 +5633,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
let type: Type | undefined;
let isTypeIncomplete = false;
let narrowedTypeForSet: Type | undefined;
if (memberInfo.symbol.isInitVar()) {
diag?.addMessage(LocAddendum.memberIsInitVar().format({ name: memberName }));
@ -5830,6 +5849,15 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
});
if (!isDescriptorError && usage.method === 'set' && usage.setType) {
if (errorNode && memberInfo.symbol.hasTypedDeclarations()) {
// This is an assignment to a member with a declared type. Apply
// narrowing logic based on the assigned type. Skip this for
// descriptor-based accesses.
narrowedTypeForSet = isDescriptorApplied
? usage.setType.type
: narrowTypeBasedOnAssignment(errorNode, type, usage.setType.type);
}
// Verify that the assigned type is compatible.
if (!assignType(type, usage.setType.type, diag?.createAddendum())) {
if (!usage.setType.isIncomplete) {
@ -5869,6 +5897,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
isClassVar: memberInfo.isClassVar,
classType: memberInfo.classType,
isAsymmetricAccessor,
narrowedTypeForSet,
memberAccessDeprecationInfo,
};
}

View File

@ -205,6 +205,10 @@ export interface TypeResult<T extends Type = Type> {
// corresponding __getattr__?
isAsymmetricAccessor?: boolean;
// For member access operations that are 'set', this is the narrowed
// type when considering the declared type of the member.
narrowedTypeForSet?: Type | undefined;
// Is the type wrapped in a "Required", "NotRequired" or "ReadOnly" class?
isRequired?: boolean;
isNotRequired?: boolean;
@ -412,6 +416,10 @@ export interface ClassMemberLookup {
// to __get__ and __set__ types?
isAsymmetricAccessor: boolean;
// For member access operations that are 'set', this is the narrowed
// type when considering the declared type of the member.
narrowedTypeForSet?: Type;
// Deprecation messages related to magic methods invoked via the member access.
memberAccessDeprecationInfo?: MemberAccessDeprecationInfo;
}

View File

@ -1,8 +1,10 @@
# This sample tests the type checker's handling of
# member assignments.
from not_present import NotPresentClass # type: ignore
class Foo:
class ClassA:
def __init__(self):
self.string_list: list[str] = []
@ -10,7 +12,7 @@ class Foo:
return ""
a = Foo()
a = ClassA()
a.string_list = ["yep"]
@ -46,13 +48,25 @@ a.do_something = lambda: "hello"
a.do_something = lambda x: 1
Foo.do_something = patch2
ClassA.do_something = patch2
# This should generate an error because of a param count mismatch
Foo.do_something = patch1
ClassA.do_something = patch1
class Class1:
class ClassB:
# This should generate an error because assignment expressions
# can't be used within a class.
[(j := i) for i in range(5)]
class ClassC:
def __init__(self):
self.x = NotPresentClass # type: int
self.y: int
self.y = NotPresentClass
self.z: int = NotPresentClass
reveal_type(self.x, expected_text="Unknown | int")
reveal_type(self.y, expected_text="Unknown | int")
reveal_type(self.z, expected_text="Unknown | int")