From d7c5d0691a7292646772898d927156998a2fa57c Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Wed, 3 Jul 2024 12:27:02 -0700 Subject: [PATCH] Fixed regression that results in a false positive when a traditional type alias (not using PEP 695 syntax) defines a union and is then used as a second argument to an `isinstance` or `issubclass` call. This addresses #8302. (#8303) --- .../src/analyzer/typeEvaluator.ts | 11 +++++- .../src/analyzer/typeEvaluatorTypes.ts | 4 +++ .../tests/samples/typeNarrowingIsinstance1.py | 36 +++++++++++++------ 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/packages/pyright-internal/src/analyzer/typeEvaluator.ts b/packages/pyright-internal/src/analyzer/typeEvaluator.ts index 77f47cc16..afdbc208b 100644 --- a/packages/pyright-internal/src/analyzer/typeEvaluator.ts +++ b/packages/pyright-internal/src/analyzer/typeEvaluator.ts @@ -4617,6 +4617,14 @@ export function createTypeEvaluator( return type; } + // Isinstance treats traditional (non-PEP 695) type aliases that are unions + // as tuples of classes rather than unions. + if ((flags & EvalFlags.IsinstanceParam) !== 0) { + if (isUnion(type) && type.typeAliasInfo && !type.typeAliasInfo.isPep695Syntax) { + return type; + } + } + return type.specialForm ?? type; } @@ -12055,7 +12063,8 @@ export function createTypeEvaluator( EvalFlags.NoParamSpec | EvalFlags.NoTypeVarTuple | EvalFlags.NoFinal | - EvalFlags.NoSpecialize + EvalFlags.NoSpecialize | + EvalFlags.IsinstanceParam : EvalFlags.NoFinal | EvalFlags.NoSpecialize; const exprTypeResult = getTypeOfExpression( argParam.argument.valueExpression, diff --git a/packages/pyright-internal/src/analyzer/typeEvaluatorTypes.ts b/packages/pyright-internal/src/analyzer/typeEvaluatorTypes.ts index 67de08de5..3c06e6d80 100644 --- a/packages/pyright-internal/src/analyzer/typeEvaluatorTypes.ts +++ b/packages/pyright-internal/src/analyzer/typeEvaluatorTypes.ts @@ -158,6 +158,10 @@ export const enum EvalFlags { // other container classes). StripTupleLiterals = 1 << 28, + // Interpret the expression using the specialized behaviors associated + // with the second argument to isinstance and issubclass calls. + IsinstanceParam = 1 << 29, + // Defaults used for evaluating the LHS of a call expression. CallBaseDefaults = NoSpecialize, diff --git a/packages/pyright-internal/src/tests/samples/typeNarrowingIsinstance1.py b/packages/pyright-internal/src/tests/samples/typeNarrowingIsinstance1.py index 84e694723..9ddc2fc22 100644 --- a/packages/pyright-internal/src/tests/samples/typeNarrowingIsinstance1.py +++ b/packages/pyright-internal/src/tests/samples/typeNarrowingIsinstance1.py @@ -1,7 +1,7 @@ # This sample exercises the type analyzer's isinstance type narrowing logic. from types import NoneType -from typing import Generic, Protocol, Sized, TypeVar, Union, Any, runtime_checkable +from typing import Any, Generic, Protocol, Sized, TypeVar, Union, runtime_checkable S = TypeVar("S") T = TypeVar("T") @@ -151,11 +151,25 @@ def handler(node: Base1) -> Any: reveal_type(node.value, expected_text="Sub1_1") -def func8(a: int | list[int] | dict[str, int] | None): - if isinstance( - a, - (str, (int, list, type(None))), - ): +def func8a(a: int | list[int] | dict[str, int] | None): + if isinstance(a, (str, (int, list, type(None)))): + reveal_type(a, expected_text="int | list[int] | None") + else: + reveal_type(a, expected_text="dict[str, int]") + + +def func8b(a: int | list[int] | dict[str, int] | None): + if isinstance(a, str | int | list | type(None)): + reveal_type(a, expected_text="int | list[int] | None") + else: + reveal_type(a, expected_text="dict[str, int]") + + +TA1 = str | int | list | None + + +def func8c(a: int | list[int] | dict[str, int] | None): + if isinstance(a, TA1): reveal_type(a, expected_text="int | list[int] | None") else: reveal_type(a, expected_text="dict[str, int]") @@ -200,12 +214,12 @@ def func11(x: Proto1): reveal_type(x, expected_text="Proto1") -TA1 = list["TA2"] | dict[str, "TA2"] -TA2 = str | TA1 +TA2 = list["TA3"] | dict[str, "TA3"] +TA3 = str | TA2 -def func12(x: TA2) -> None: +def func12(x: TA3) -> None: if isinstance(x, dict): - reveal_type(x, expected_text="dict[str, str | list[TA2] | dict[str, TA2]]") + reveal_type(x, expected_text="dict[str, str | list[TA3] | dict[str, TA3]]") else: - reveal_type(x, expected_text="str | list[str | list[TA2] | dict[str, TA2]]") + reveal_type(x, expected_text="str | list[str | list[TA3] | dict[str, TA3]]")