From ca078abc07b25bf8e9fceacf44e08b72b248a9fc Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Wed, 4 Jan 2023 12:57:41 -0700 Subject: [PATCH] Added support for `NoneType` to be used in an `isinstance` type guard or match statement. This addresses https://github.com/microsoft/pyright/issues/4402. --- .../src/analyzer/patternMatching.ts | 4 ++-- .../src/analyzer/typeGuards.ts | 8 +++++++- .../pyright-internal/src/analyzer/typeUtils.ts | 5 +++++ .../src/tests/samples/match10.py | 18 ++++++++++++++++-- .../tests/samples/typeNarrowingIsinstance1.py | 8 ++++++++ 5 files changed, 38 insertions(+), 5 deletions(-) diff --git a/packages/pyright-internal/src/analyzer/patternMatching.ts b/packages/pyright-internal/src/analyzer/patternMatching.ts index 4f47f0d2822f..449592fe601d 100644 --- a/packages/pyright-internal/src/analyzer/patternMatching.ts +++ b/packages/pyright-internal/src/analyzer/patternMatching.ts @@ -503,7 +503,7 @@ function narrowTypeBasedOnClassPattern( type, /* conditionFilter */ undefined, (subjectSubtypeExpanded, subjectSubtypeUnexpanded) => { - if (!isClassInstance(subjectSubtypeExpanded)) { + if (!isNoneInstance(subjectSubtypeExpanded) && !isClassInstance(subjectSubtypeExpanded)) { return subjectSubtypeUnexpanded; } @@ -521,7 +521,7 @@ function narrowTypeBasedOnClassPattern( // if the types match exactly or the subtype is a final class and // therefore cannot be subclassed. if (!evaluator.assignType(subjectSubtypeExpanded, classInstance)) { - if (!ClassType.isFinal(subjectSubtypeExpanded)) { + if (isClass(subjectSubtypeExpanded) && !ClassType.isFinal(subjectSubtypeExpanded)) { return subjectSubtypeExpanded; } } diff --git a/packages/pyright-internal/src/analyzer/typeGuards.ts b/packages/pyright-internal/src/analyzer/typeGuards.ts index 836dd4cacdaa..04280d1d3bb3 100644 --- a/packages/pyright-internal/src/analyzer/typeGuards.ts +++ b/packages/pyright-internal/src/analyzer/typeGuards.ts @@ -1366,7 +1366,13 @@ function narrowTypeForIsInstance( if (isInstanceCheck) { if (isNoneInstance(subtype)) { - const containsNoneType = classTypeList.some((t) => isNoneTypeClass(t)); + const containsNoneType = classTypeList.some((t) => { + if (isNoneTypeClass(t)) { + return true; + } + return isInstantiableClass(t) && ClassType.isBuiltIn(t, 'NoneType'); + }); + if (isPositiveTest) { return containsNoneType ? subtype : undefined; } else { diff --git a/packages/pyright-internal/src/analyzer/typeUtils.ts b/packages/pyright-internal/src/analyzer/typeUtils.ts index b2b3ecf7e272..76121238d3d5 100644 --- a/packages/pyright-internal/src/analyzer/typeUtils.ts +++ b/packages/pyright-internal/src/analyzer/typeUtils.ts @@ -1941,6 +1941,11 @@ export function convertToInstance(type: Type, includeSubclasses = true): Type { } } + // Handle NoneType as a special case. + if (TypeBase.isInstantiable(subtype) && ClassType.isBuiltIn(subtype, 'NoneType')) { + return NoneType.createInstance(); + } + return ClassType.cloneAsInstance(subtype, includeSubclasses); } diff --git a/packages/pyright-internal/src/tests/samples/match10.py b/packages/pyright-internal/src/tests/samples/match10.py index 1f0dffcad067..1ce95b8309a6 100644 --- a/packages/pyright-internal/src/tests/samples/match10.py +++ b/packages/pyright-internal/src/tests/samples/match10.py @@ -1,14 +1,16 @@ # This sample tests the reportMatchNotExhaustive diagnostic check. +from types import NoneType from typing import Literal from enum import Enum + def func1(subj: Literal["a", "b"], cond: bool): # This should generate an error if reportMatchNotExhaustive is enabled. match subj: case "a": pass - + case "b" if cond: pass @@ -19,11 +21,13 @@ def func2(subj: object): case int(): pass + def func3(subj: object): match subj: case object(): pass + def func4(subj: tuple[str] | tuple[int]): match subj[0]: case str(): @@ -32,15 +36,17 @@ def func4(subj: tuple[str] | tuple[int]): case int(): pass + def func5(subj: Literal[1, 2, 3]): # This should generate an error if reportMatchNotExhaustive is enabled. match subj: case 1 | 2: pass + class Color(Enum): red = 0 - green= 1 + green = 1 blue = 2 @@ -70,8 +76,16 @@ def func7() -> int: class SingleColor(Enum): red = 0 + def func8(x: SingleColor) -> int: match x: case SingleColor.red: return 1 + +def func9(x: int | None): + match x: + case NoneType(): + return 1 + case int(): + return 2 diff --git a/packages/pyright-internal/src/tests/samples/typeNarrowingIsinstance1.py b/packages/pyright-internal/src/tests/samples/typeNarrowingIsinstance1.py index 851121cbf486..133cd253777d 100644 --- a/packages/pyright-internal/src/tests/samples/typeNarrowingIsinstance1.py +++ b/packages/pyright-internal/src/tests/samples/typeNarrowingIsinstance1.py @@ -1,5 +1,6 @@ # This sample exercises the type analyzer's isinstance type narrowing logic. +from types import NoneType from typing import List, Optional, Sized, Type, TypeVar, Union, Any @@ -160,3 +161,10 @@ def func8(a: int | list[int] | dict[str, int] | None): reveal_type(a, expected_text="int | list[int] | None") else: reveal_type(a, expected_text="dict[str, int]") + + +def func9(a: int | None): + if not isinstance(a, NoneType): + reveal_type(a, expected_text="int") + else: + reveal_type(a, expected_text="None")