Added provisional support for draft PEP 742 (TypeIs).

This commit is contained in:
Eric Traut 2024-02-19 01:03:55 -07:00
parent 1700e5451b
commit 08d3f6d38c
11 changed files with 196 additions and 267 deletions

View File

@ -4101,6 +4101,7 @@ export class Binder extends ParseTreeWalker {
'Never',
'LiteralString',
'OrderedDict',
'TypeIs',
]);
const assignedName = assignedNameNode.value;

View File

@ -660,7 +660,7 @@ export class Checker extends ParseTreeWalker {
// Verify common dunder signatures.
this._validateDunderSignatures(node, functionTypeResult.functionType, containingClassNode !== undefined);
// Verify TypeGuard functions.
// Verify TypeGuard and TypeIs functions.
this._validateTypeGuardFunction(node, functionTypeResult.functionType, containingClassNode !== undefined);
this._validateFunctionTypeVarUsage(node, functionTypeResult);
@ -4589,7 +4589,10 @@ export class Checker extends ParseTreeWalker {
return;
}
if (!ClassType.isBuiltIn(returnType, 'TypeGuard')) {
const isTypeGuard = ClassType.isBuiltIn(returnType, 'TypeGuard');
const isTypeIs = ClassType.isBuiltIn(returnType, 'TypeIs');
if (!isTypeGuard && !isTypeIs) {
return;
}
@ -4612,6 +4615,34 @@ export class Checker extends ParseTreeWalker {
node.name
);
}
if (isTypeIs) {
const typeGuardType = returnType.typeArguments[0];
// Determine the type of the first parameter.
const paramIndex = isMethod && !FunctionType.isStaticMethod(functionType) ? 1 : 0;
if (paramIndex >= functionType.details.parameters.length) {
return;
}
const paramType = FunctionType.getEffectiveParameterType(functionType, paramIndex);
// Verify that the typeGuardType is a narrower type than the paramType.
if (!this._evaluator.assignType(paramType, typeGuardType)) {
const returnAnnotation =
node.returnTypeAnnotation || node.functionAnnotationComment?.returnTypeAnnotation;
if (returnAnnotation) {
this._evaluator.addDiagnostic(
DiagnosticRule.reportGeneralTypeIssues,
LocMessage.typeIsReturnType().format({
type: this._evaluator.printType(paramType),
returnType: this._evaluator.printType(typeGuardType),
}),
returnAnnotation
);
}
}
}
}
private _validateDunderSignatures(node: FunctionNode, functionType: FunctionType, isMethod: boolean) {

View File

@ -11418,16 +11418,13 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
specializedReturnType = ClassType.cloneForUnpacked(specializedReturnType, /* isUnpackedTuple */ false);
}
// Handle 'TypeGuard' specially. We'll transform the return type into a 'bool'
// object with a type argument that reflects the narrowed type.
// Handle 'TypeGuard' and 'TypeIs' specially. We'll transform the return type
// into a 'bool' object with a type argument that reflects the narrowed type.
if (
isClassInstance(specializedReturnType) &&
ClassType.isBuiltIn(specializedReturnType, 'TypeGuard') &&
ClassType.isBuiltIn(specializedReturnType, ['TypeGuard', 'TypeIs']) &&
specializedReturnType.typeArguments &&
specializedReturnType.typeArguments.length > 0 &&
isClassInstance(returnType) &&
returnType.typeArguments &&
returnType.typeArguments.length > 0
specializedReturnType.typeArguments.length > 0
) {
if (boolClassType && isInstantiableClass(boolClassType)) {
let typeGuardType = specializedReturnType.typeArguments[0];
@ -11447,25 +11444,9 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
}
}
let useStrictTypeGuardSemantics = false;
if (AnalyzerNodeInfo.getFileInfo(errorNode).diagnosticRuleSet.enableExperimentalFeatures) {
// Determine the type of the first parameter.
const paramIndex = type.boundToType ? 1 : 0;
if (paramIndex < type.details.parameters.length) {
const paramType = FunctionType.getEffectiveParameterType(type, paramIndex);
// If the type guard meets the requirements that the first parameter
// type is a proper subtype of the return type, we can use strict
// type guard semantics.
if (assignType(paramType, returnType.typeArguments[0])) {
useStrictTypeGuardSemantics = true;
}
}
}
const useTypeIsSemantics = ClassType.isBuiltIn(specializedReturnType, 'TypeIs');
specializedReturnType = ClassType.cloneAsInstance(
ClassType.cloneForTypeGuard(boolClassType, typeGuardType, useStrictTypeGuardSemantics)
ClassType.cloneForTypeGuard(boolClassType, typeGuardType, useTypeIsSemantics)
);
}
}
@ -14649,7 +14630,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
return type;
}
// Creates a "TypeGuard" type. This is an alias for 'bool', which
// Creates a "TypeGuard" and "TypeIs" type. This is an alias for 'bool', which
// isn't a generic type and therefore doesn't have a typeParameter.
// We'll abuse our internal types a bit by specializing it with
// a type argument anyway.
@ -15437,6 +15418,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
['Never', { alias: '', module: 'builtins', isSpecialForm: true }],
['LiteralString', { alias: '', module: 'builtins', isSpecialForm: true }],
['ReadOnly', { alias: '', module: 'builtins', isSpecialForm: true }],
['TypeIs', { alias: '', module: 'builtins', isSpecialForm: true }],
]);
const aliasMapEntry = specialTypes.get(assignedName);
@ -19445,7 +19427,8 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
return { type: createConcatenateType(classType, errorNode, typeArgs, flags) };
}
case 'TypeGuard': {
case 'TypeGuard':
case 'TypeIs': {
return { type: createTypeGuardType(classType, errorNode, typeArgs, flags) };
}
@ -23189,7 +23172,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
const isLiteral = isClass(srcType) && srcType.literalValue !== undefined;
return !isLiteral;
}
} else if (ClassType.isBuiltIn(destType, 'TypeGuard')) {
} else if (ClassType.isBuiltIn(destType, ['TypeGuard', 'TypeIs'])) {
// All the source to be a "bool".
if ((originalFlags & AssignTypeFlags.AllowBoolTypeGuard) !== 0) {
if (isClassInstance(srcType) && ClassType.isBuiltIn(srcType, 'bool')) {
@ -25058,11 +25041,12 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
) {
isReturnTypeCompatible = true;
} else {
// Handle the special case where the return type is a TypeGuard[T].
// This should also act as a bool, since that's its type at runtime.
// Handle the special case where the return type is a TypeGuard[T]
// or TypeIs[T]. This should also act as a bool, since that's its
// type at runtime.
if (
isClassInstance(srcReturnType) &&
ClassType.isBuiltIn(srcReturnType, 'TypeGuard') &&
ClassType.isBuiltIn(srcReturnType, ['TypeGuard', 'TypeIs']) &&
boolClassType &&
isInstantiableClass(boolClassType)
) {

View File

@ -715,7 +715,7 @@ export function getTypeNarrowingCallback(
return (
type.details.declaredReturnType &&
isClassInstance(type.details.declaredReturnType) &&
ClassType.isBuiltIn(type.details.declaredReturnType, 'TypeGuard')
ClassType.isBuiltIn(type.details.declaredReturnType, ['TypeGuard', 'TypeIs'])
);
};

View File

@ -2933,6 +2933,7 @@ export function requiresTypeArguments(classType: ClassType) {
'Literal',
'Annotated',
'TypeGuard',
'TypeIs',
];
if (specialClasses.some((t) => t === (classType.aliasName || classType.details.name))) {

View File

@ -980,6 +980,8 @@ export namespace Localizer {
new ParameterizedString<{ type: string }>(getRawString('Diagnostic.typeExpectedClass'));
export const typeGuardArgCount = () => getRawString('Diagnostic.typeGuardArgCount');
export const typeGuardParamCount = () => getRawString('Diagnostic.typeGuardParamCount');
export const typeIsReturnType = () =>
new ParameterizedString<{ type: string; returnType: string }>(getRawString('Diagnostic.typeIsReturnType'));
export const typeNotAwaitable = () =>
new ParameterizedString<{ type: string }>(getRawString('Diagnostic.typeNotAwaitable'));
export const typeNotIntantiable = () =>

View File

@ -490,8 +490,9 @@
"typedDictSecondArgDictEntry": "Expected simple dictionary entry",
"typedDictSet": "Could not assign item in TypedDict",
"typeExpectedClass": "Expected type expression but received \"{type}\"",
"typeGuardArgCount": "Expected a single type argument after \"TypeGuard\"",
"typeGuardArgCount": "Expected a single type argument after \"TypeGuard\" or \"TypeIs\"",
"typeGuardParamCount": "User-defined type guard functions and methods must have at least one input parameter",
"typeIsReturnType": "Return type of TypeIs (\"{returnType}\") is not consistent with value parameter type (\"{type}\")",
"typeNotAwaitable": "\"{type}\" is not awaitable",
"typeNotIntantiable": "\"{type}\" cannot be instantiated",
"typeNotIterable": "\"{type}\" is not iterable",

View File

@ -1,194 +1,54 @@
# This sample tests the TypeGuard using "strict" semantics.
# This sample tests that user-defined TypeGuard can
# be used in an overloaded function.
from typing import Any, Literal, Mapping, Sequence, TypeVar, Union
from typing_extensions import TypeGuard
from enum import Enum
from typing import Literal, overload
from typing_extensions import TypeGuard, TypeIs
def is_str1(val: Union[str, int]) -> TypeGuard[str]:
return isinstance(val, str)
class TypeGuardMode(Enum):
NoTypeGuard = 0
TypeGuard = 1
TypeIs = 2
def func1(val: Union[str, int]):
if is_str1(val):
reveal_type(val, expected_text="str")
@overload
def is_int(obj: object, mode: Literal[TypeGuardMode.NoTypeGuard]) -> bool:
...
@overload
def is_int(obj: object, mode: Literal[TypeGuardMode.TypeGuard]) -> TypeGuard[int]:
...
@overload
def is_int(
obj: object, mode: Literal[TypeGuardMode.TypeIs]
) -> TypeIs[int]:
...
def is_int(obj: object, mode: TypeGuardMode) -> bool | TypeGuard[int] | TypeIs[int]:
...
def func_no_typeguard(val: int | str):
if is_int(val, TypeGuardMode.NoTypeGuard):
reveal_type(val, expected_text="int | str")
else:
reveal_type(val, expected_text="int | str")
def func_typeguard(val: int | str):
if is_int(val, TypeGuardMode.TypeGuard):
reveal_type(val, expected_text="int")
def is_true(o: object) -> TypeGuard[Literal[True]]:
...
def func2(val: bool):
if not is_true(val):
reveal_type(val, expected_text="bool")
else:
reveal_type(val, expected_text="Literal[True]")
reveal_type(val, expected_text="bool")
reveal_type(val, expected_text="int | str")
def is_list(val: object) -> TypeGuard[list[Any]]:
return isinstance(val, list)
def func3(val: dict[str, str] | list[str] | list[int] | Sequence[int]):
if is_list(val):
reveal_type(val, expected_text="list[str] | list[int] | list[Any]")
def func_typeis(val: int | str):
if is_int(val, TypeGuardMode.TypeIs):
reveal_type(val, expected_text="int")
else:
reveal_type(val, expected_text="dict[str, str] | Sequence[int]")
def func4(val: dict[str, str] | list[str] | list[int] | tuple[int]):
if is_list(val):
reveal_type(val, expected_text="list[str] | list[int]")
else:
reveal_type(val, expected_text="dict[str, str] | tuple[int]")
_K = TypeVar("_K")
_V = TypeVar("_V")
def is_dict(val: Mapping[_K, _V]) -> TypeGuard[dict[_K, _V]]:
return isinstance(val, dict)
def func5(val: dict[_K, _V] | Mapping[_K, _V]):
if not is_dict(val):
reveal_type(val, expected_text="Mapping[_K@func5, _V@func5]")
else:
reveal_type(val, expected_text="dict[_K@func5, _V@func5]")
def is_cardinal_direction(val: str) -> TypeGuard[Literal["N", "S", "E", "W"]]:
return val in ("N", "S", "E", "W")
def func6(direction: Literal["NW", "E"]):
if is_cardinal_direction(direction):
reveal_type(direction, expected_text="Literal['E']")
else:
reveal_type(direction, expected_text="Literal['NW']")
class Animal:
...
class Kangaroo(Animal):
...
class Koala(Animal):
...
T = TypeVar("T")
def is_marsupial(val: Animal) -> TypeGuard[Kangaroo | Koala]:
return isinstance(val, Kangaroo | Koala)
class A1:
...
class A2(A1):
...
class B1:
...
class B2(B1):
...
class C1:
...
class C2(C1):
...
def guard1(val: A1 | B1 | C1) -> TypeGuard[A1 | B1]:
return isinstance(val, (A1, B1))
def func7_1(val: A1 | B1 | C1):
if guard1(val):
reveal_type(val, expected_text="A1 | B1")
else:
reveal_type(val, expected_text="C1")
def func7_2(val: A2 | B2 | C2):
if guard1(val):
reveal_type(val, expected_text="A2 | B2")
else:
reveal_type(val, expected_text="C2")
def func7_3(val: A2 | B2 | C2 | Any):
if guard1(val):
reveal_type(val, expected_text="A2 | B2 | A1 | B1")
else:
reveal_type(val, expected_text="C2 | Any")
def guard2(val: A1 | B1 | C1) -> TypeGuard[A2 | B2]:
return isinstance(val, (A2, B2))
def func8_1(val: A1 | B1 | C1):
if guard2(val):
reveal_type(val, expected_text="A2 | B2")
else:
reveal_type(val, expected_text="A1 | B1 | C1")
def func8_2(val: A2 | B2 | C2):
if guard2(val):
reveal_type(val, expected_text="A2 | B2")
else:
reveal_type(val, expected_text="C2")
def func8_3(val: A2 | B2 | C2 | Any):
if guard2(val):
reveal_type(val, expected_text="A2 | B2")
else:
reveal_type(val, expected_text="C2 | Any")
def guard3(val: A2 | B2 | C2) -> TypeGuard[A2 | B2 | Any]:
return isinstance(val, (A1, B1))
def func3_1(val: A2 | C2):
if guard3(val):
reveal_type(val, expected_text="A2 | C2")
else:
reveal_type(val, expected_text="C2")
def func3_2(val: A2 | B2 | C2 | Any):
if guard3(val):
reveal_type(val, expected_text="A2 | B2 | C2 | Any")
else:
reveal_type(val, expected_text="C2 | Any")
def guard4(o: type) -> TypeGuard[type[A1]]:
...
def func4_1(cls: type):
if guard4(cls):
reveal_type(cls, expected_text="type[A1]")
else:
reveal_type(cls, expected_text="type")
reveal_type(val, expected_text="str")

View File

@ -1,39 +0,0 @@
# This sample tests that user-defined TypeGuard can
# be used in an overloaded function.
from enum import Enum
from typing import Literal, overload
from typing_extensions import TypeGuard
class TypeGuardMode(Enum):
NoTypeGuard = 0
TypeGuard = 1
@overload
def is_int(obj: object, mode: Literal[TypeGuardMode.NoTypeGuard]) -> bool:
...
@overload
def is_int(obj: object, mode: Literal[TypeGuardMode.TypeGuard]) -> TypeGuard[int]:
...
def is_int(obj: object, mode: TypeGuardMode) -> bool | TypeGuard[int]:
...
def func_no_typeguard(val: int | str):
if is_int(val, TypeGuardMode.NoTypeGuard):
reveal_type(val, expected_text="int | str")
else:
reveal_type(val, expected_text="int | str")
def func_typeguard(val: int | str):
if is_int(val, TypeGuardMode.TypeGuard):
reveal_type(val, expected_text="int")
else:
reveal_type(val, expected_text="str")

View File

@ -0,0 +1,94 @@
# This sample tests the TypeIs form.
from typing import Any, Literal, Mapping, Sequence, TypeVar, Union
from typing_extensions import TypeIs
def is_str1(val: Union[str, int]) -> TypeIs[str]:
return isinstance(val, str)
def func1(val: Union[str, int]):
if is_str1(val):
reveal_type(val, expected_text="str")
else:
reveal_type(val, expected_text="int")
def is_true(o: object) -> TypeIs[Literal[True]]: ...
def func2(val: bool):
if not is_true(val):
reveal_type(val, expected_text="bool")
else:
reveal_type(val, expected_text="Literal[True]")
reveal_type(val, expected_text="bool")
def is_list(val: object) -> TypeIs[list[Any]]:
return isinstance(val, list)
def func3(val: dict[str, str] | list[str] | list[int] | Sequence[int]):
if is_list(val):
reveal_type(val, expected_text="list[str] | list[int] | list[Any]")
else:
reveal_type(val, expected_text="dict[str, str] | Sequence[int]")
def func4(val: dict[str, str] | list[str] | list[int] | tuple[int]):
if is_list(val):
reveal_type(val, expected_text="list[str] | list[int]")
else:
reveal_type(val, expected_text="dict[str, str] | tuple[int]")
_K = TypeVar("_K")
_V = TypeVar("_V")
def is_dict(val: Mapping[_K, _V]) -> TypeIs[dict[_K, _V]]:
return isinstance(val, dict)
def func5(val: dict[_K, _V] | Mapping[_K, _V]):
if not is_dict(val):
reveal_type(val, expected_text="Mapping[_K@func5, _V@func5]")
else:
reveal_type(val, expected_text="dict[_K@func5, _V@func5]")
def is_cardinal_direction(val: str) -> TypeIs[Literal["N", "S", "E", "W"]]:
return val in ("N", "S", "E", "W")
def func6(direction: Literal["NW", "E"]):
if is_cardinal_direction(direction):
reveal_type(direction, expected_text="Literal['E']")
else:
reveal_type(direction, expected_text="Literal['NW']")
class Animal: ...
class Kangaroo(Animal): ...
class Koala(Animal): ...
T = TypeVar("T")
def is_marsupial(val: Animal) -> TypeIs[Kangaroo | Koala]:
return isinstance(val, Kangaroo | Koala)
# This should generate an error because list[T] isn't consistent with list[T | None].
def has_no_nones(
val: list[T | None],
) -> TypeIs[list[T]]:
return None not in val

View File

@ -987,19 +987,13 @@ test('TypeGuard2', () => {
});
test('TypeGuard3', () => {
const configOptions = new ConfigOptions(Uri.empty());
configOptions.diagnosticRuleSet.enableExperimentalFeatures = true;
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeGuard3.py'], configOptions);
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeGuard3.py']);
TestUtils.validateResults(analysisResults, 0);
});
test('TypeGuard4', () => {
const configOptions = new ConfigOptions(Uri.empty());
configOptions.diagnosticRuleSet.enableExperimentalFeatures = true;
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeGuard4.py'], configOptions);
TestUtils.validateResults(analysisResults, 0);
test('TypeIs1', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeIs1.py']);
TestUtils.validateResults(analysisResults, 1);
});
test('Never1', () => {