mirror of
https://github.com/microsoft/pyright.git
synced 2024-09-11 07:55:56 +03:00
Implemented provisional support for PEP 746, which provides consistency checks for metadata used in an Annotated
annotation.
This commit is contained in:
parent
7c5495c1b4
commit
ac7f6b7f74
@ -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.
|
||||
|
@ -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 = () =>
|
||||
|
@ -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\"",
|
||||
|
@ -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
|
@ -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']);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user