Implemented provisional support for PEP 746, which provides consistency checks for metadata used in an Annotated annotation.

This commit is contained in:
Eric Traut 2024-06-05 20:24:36 -07:00
parent 7c5495c1b4
commit ac7f6b7f74
5 changed files with 162 additions and 4 deletions

View File

@ -15450,11 +15450,15 @@ export function createTypeEvaluator(
function createAnnotatedType(
classType: ClassType,
errorNode: ParseNode,
errorNode: ExpressionNode,
typeArgs: TypeResultWithNode[] | undefined
): TypeResult {
if (typeArgs && typeArgs.length < 2) {
addError(LocMessage.annotatedTypeArgMissing(), errorNode);
if (typeArgs) {
if (typeArgs.length < 2) {
addError(LocMessage.annotatedTypeArgMissing(), errorNode);
} else {
validateAnnotatedMetadata(errorNode, typeArgs[0].type, typeArgs.slice(1));
}
}
if (!typeArgs || typeArgs.length === 0) {
@ -15473,6 +15477,63 @@ export function createTypeEvaluator(
};
}
// Enforces metadata consistency as specified in PEP 746.
function validateAnnotatedMetadata(errorNode: ExpressionNode, annotatedType: Type, metaArgs: TypeResultWithNode[]) {
const fileInfo = AnalyzerNodeInfo.getFileInfo(errorNode);
// This is an experimental feature because PEP 746 hasn't been accepted.
if (!fileInfo.diagnosticRuleSet.enableExperimentalFeatures) {
return;
}
for (const metaArg of metaArgs) {
if (isClass(metaArg.type)) {
const supportsTypeMethod = getTypeOfBoundMember(
/* errorNode */ undefined,
metaArg.type,
'__supports_type__'
)?.type;
if (!supportsTypeMethod) {
continue;
}
// "Call" the __supports_type__ method to determine if the type is supported.
const argList: FunctionArgument[] = [
{
argumentCategory: ArgumentCategory.Simple,
typeResult: { type: convertToInstance(annotatedType) },
},
];
const callResult = useSpeculativeMode(errorNode, () =>
validateCallArguments(
errorNode,
argList,
{ type: supportsTypeMethod },
/* typeVarContext */ undefined,
/* skipUnknownArgCheck */ true,
/* inferenceContext */ undefined,
/* signatureTracker */ undefined
)
);
if (!callResult.isTypeIncomplete && callResult.returnType) {
if (callResult.argumentErrors || !canBeTruthy(callResult.returnType)) {
addDiagnostic(
DiagnosticRule.reportInvalidTypeArguments,
LocMessage.annotatedMetadataInconsistent().format({
metadataType: printType(metaArg.type),
type: printType(convertToInstance(annotatedType)),
}),
metaArg.node
);
}
}
}
}
}
// Creates one of several "special" types that are defined in typing.pyi
// but not declared in their entirety. This includes the likes of "Tuple",
// "Dict", etc.
@ -19933,7 +19994,7 @@ export function createTypeEvaluator(
classType: ClassType,
typeArgs: TypeResultWithNode[] | undefined,
flags: EvaluatorFlags,
errorNode: ParseNode
errorNode: ExpressionNode
): TypeResult {
// Handle the special-case classes that are not defined
// in the type stubs.

View File

@ -188,6 +188,10 @@ export function loadStringsForLocale(locale: string, localeMap: Map<string, any>
export namespace Localizer {
export namespace Diagnostic {
export const annotatedMetadataInconsistent = () =>
new ParameterizedString<{ type: string; metadataType: string }>(
getRawString('Diagnostic.annotatedMetadataInconsistent')
);
export const abstractMethodInvocation = () =>
new ParameterizedString<{ method: string }>(getRawString('Diagnostic.abstractMethodInvocation'));
export const annotatedParamCountMismatch = () =>

View File

@ -1,5 +1,6 @@
{
"Diagnostic": {
"annotatedMetadataInconsistent": "Annotated metadata type \"{metadataType}\" is not compatible with type \"{type}\"",
"abstractMethodInvocation": "Method \"{method}\" cannot be called because it is abstract and unimplemented",
"annotatedParamCountMismatch": "Parameter annotation count mismatch: expected {expected} but received {received}",
"annotatedTypeArgMissing": "Expected one type argument and one or more annotations for \"Annotated\"",

View File

@ -0,0 +1,84 @@
# This sample tests the mechanism defined in PEP 746 for validating
# the consistency of Annotated metadata with its corresponding type.
from typing import Annotated, Any, Callable, Literal, Protocol, overload
class SupportsGt[T](Protocol):
def __gt__(self, __other: T) -> bool: ...
class Gt[T]:
def __init__(self, value: T) -> None:
self.value = value
def __supports_type__(self, obj: SupportsGt[T]) -> bool:
return obj > self.value
x1: Annotated[int, Gt(0)] = 1
# This should generate an error because int is not compatible with str.
x2: Annotated[str, Gt(0)] = ""
x3: Annotated[int, Gt(1)] = 0
class MetaString:
@classmethod
def __supports_type__(cls, obj: str) -> bool:
return isinstance(obj, str)
s1: Annotated[str, MetaString] = ""
# This should generate an error because int is not compatible with str.
s2: Annotated[int, MetaString] = 1
class ParentA: ...
class ChildA(ParentA): ...
class MetaA:
def __supports_type__(self, obj: ParentA) -> bool:
return isinstance(ParentA, str)
a1: Annotated[ParentA, MetaA()] = ParentA()
a2: Annotated[ChildA, MetaA(), 1, ""] = ChildA()
# This should generate an error.
a3: Annotated[int, MetaA()] = 1
class MetaInt:
__supports_type__: Callable[[int], bool]
i1: Annotated[int, MetaInt()] = 1
# This should generate an error.
i2: Annotated[float, MetaInt()] = 1.0
class MetaWithOverride:
@overload
def __supports_type__(self, obj: int, /) -> bool: ...
@overload
def __supports_type__(self, obj: None, /) -> Literal[True]: ...
@overload
def __supports_type__(self, obj: str, /) -> Literal[False]: ...
def __supports_type__(self, obj: Any, /) -> bool: ...
v1: Annotated[int, MetaWithOverride()] = 1
v2: Annotated[None, MetaWithOverride()] = None
# This should generate an error.
v3: Annotated[str, MetaWithOverride()] = ""
# This should generate an error.
v4: Annotated[complex, MetaWithOverride()] = 3j

View File

@ -845,6 +845,14 @@ test('Annotated2', () => {
TestUtils.validateResults(analysisResults, 0);
});
test('AnnotatedMetadata1', () => {
const configOptions = new ConfigOptions(Uri.empty());
configOptions.diagnosticRuleSet.enableExperimentalFeatures = true;
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['annotatedMetadata1.py'], configOptions);
TestUtils.validateResults(analysisResults, 6);
});
test('Circular1', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['circular1.py']);