diff --git a/packages/pyright-internal/src/analyzer/typeGuards.ts b/packages/pyright-internal/src/analyzer/typeGuards.ts index 7ee3a18d5b37..f6158a68646e 100644 --- a/packages/pyright-internal/src/analyzer/typeGuards.ts +++ b/packages/pyright-internal/src/analyzer/typeGuards.ts @@ -1246,6 +1246,19 @@ export function getIsInstanceClassTypes( if (isInstantiableClass(subtype) && ClassType.isBuiltIn(subtype, 'Callable')) { subtype = convertToInstantiable(getUnknownTypeForCallable()); + } else if (TypeBase.isInstance(subtype) && ClassType.isBuiltIn(subtype, 'type')) { + if (subtype.priv.typeArgs && subtype.priv.typeArgs.length > 0) { + const typeArg = subtype.priv.typeArgs[0]; + if (isAnyOrUnknown(typeArg)) { + foundNonClassType = true; + return; + } + if (isInstantiableClass(typeArg)) { + subtype = typeArg; + } else if (isClass(typeArg) && TypeBase.isInstance(typeArg)) { + subtype = convertToInstantiable(typeArg); + } + } } } @@ -1540,8 +1553,12 @@ function narrowTypeForInstance( // note this case specially so we don't do any narrowing, which // will generate false positives. if (filterIsSuperclass) { - if (!isTypeIsCheck && concreteFilterType.priv.includeSubclasses) { - // If the filter type includes subclasses, we can't eliminate + if ( + !isTypeIsCheck && + concreteFilterType.priv.includeSubclasses && + !ClassType.isFinal(concreteFilterType) + ) { + // If the filter type includes subclasses and is not final, we can't eliminate // this type in the negative direction. We'll relax this for // TypeIs checks. isClassRelationshipIndeterminate = true; diff --git a/packages/pyright-internal/src/tests/samples/typeNarrowingIsinstance22.py b/packages/pyright-internal/src/tests/samples/typeNarrowingIsinstance22.py new file mode 100644 index 000000000000..fef67ef0989b --- /dev/null +++ b/packages/pyright-internal/src/tests/samples/typeNarrowingIsinstance22.py @@ -0,0 +1,37 @@ +# This sample tests type narrowing for isinstance and issubclass when +# the class argument is passed as a type[T] variable or tuple of type[T]. + +# pyright: reportMissingModuleSource=false + +from typing import final +from typing_extensions import reveal_type + + +class A: + pass + + +class B: + pass + + +@final +class FinalClass: + pass + + +def test_positive_narrowing(x: A | B, cls: type[A]): + if isinstance(x, cls): + reveal_type(x, expected_text="A") + + +def test_positive_tuple_param(x: A | B, types: tuple[type[A], type[B]]): + if isinstance(x, types): + reveal_type(x, expected_text="A | B") + + +def test_final_class_param(x: FinalClass | B, cls: type[FinalClass]): + if isinstance(x, cls): + reveal_type(x, expected_text="FinalClass") + else: + reveal_type(x, expected_text="B") diff --git a/packages/pyright-internal/src/tests/typeEvaluator1.test.ts b/packages/pyright-internal/src/tests/typeEvaluator1.test.ts index 73306b3b11a4..e010e41267d8 100644 --- a/packages/pyright-internal/src/tests/typeEvaluator1.test.ts +++ b/packages/pyright-internal/src/tests/typeEvaluator1.test.ts @@ -507,6 +507,12 @@ test('TypeNarrowingIsinstance21', () => { TestUtils.validateResults(analysisResults, 0); }); +test('TypeNarrowingIsinstance22', () => { + const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeNarrowingIsinstance22.py']); + + TestUtils.validateResults(analysisResults, 0); +}); + test('TypeNarrowingTupleLength1', () => { const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeNarrowingTupleLength1.py']);