diff --git a/packages/pyright-internal/src/analyzer/typeEvaluator.ts b/packages/pyright-internal/src/analyzer/typeEvaluator.ts index 68b3343e6fae..8f55f06f9b3f 100644 --- a/packages/pyright-internal/src/analyzer/typeEvaluator.ts +++ b/packages/pyright-internal/src/analyzer/typeEvaluator.ts @@ -4011,7 +4011,8 @@ export function createTypeEvaluator( if ( TypeBase.isInstance(destType) && !isConstantName(nameValue) && - !isFinalVariable(symbolWithScope.symbol) + !isFinalVariable(symbolWithScope.symbol) && + !isSentinelLiteral(destType) ) { destType = stripTypeForm(stripLiteralValue(destType)); } @@ -24657,6 +24658,12 @@ export function createTypeEvaluator( isConstant = true; } + // Sentinel objects are singletons; retaining the literal is sound + // even when the binding is not a CONSTANT_NAME or Final. + if (isSentinelLiteral(type)) { + isConstant = true; + } + // If the symbol is constant, we can retain the literal // value and TypeForm types. Otherwise, strip literal values // and TypeForm types to widen. diff --git a/packages/pyright-internal/src/tests/samples/sentinel3.py b/packages/pyright-internal/src/tests/samples/sentinel3.py new file mode 100644 index 000000000000..bce89d62f4a3 --- /dev/null +++ b/packages/pyright-internal/src/tests/samples/sentinel3.py @@ -0,0 +1,44 @@ +# This sample tests that Sentinel values retain their literal type +# even when the binding is not a CONSTANT_NAME. + +from dataclasses import dataclass +from typing_extensions import Sentinel # pyright: ignore[reportMissingModuleSource] + + +Empty = Sentinel("Empty") +MISSING = Sentinel("MISSING") + + +def func1(value: int | Empty) -> None: + if value is Empty: + reveal_type(value, expected_text="Empty") + else: + reveal_type(value, expected_text="int") + + +def func2(value: int | Empty) -> None: + if value is not Empty: + reveal_type(value, expected_text="int") + else: + reveal_type(value, expected_text="Empty") + + +@dataclass +class Address: + email: str + name: str | None + + +def update_not_empty(address: Address, email: str | Empty, name: str | None | Empty) -> None: + if email is not Empty: + address.email = email + reveal_type(email, expected_text="str") + if name is not Empty: + address.name = name + reveal_type(name, expected_text="str | None") + + +def update_not_missing(address: Address, email: str | MISSING) -> None: + if email is not MISSING: + address.email = email + reveal_type(email, expected_text="str") diff --git a/packages/pyright-internal/src/tests/typeEvaluator2.test.ts b/packages/pyright-internal/src/tests/typeEvaluator2.test.ts index b6db2402a2f3..1f2f82b3e568 100644 --- a/packages/pyright-internal/src/tests/typeEvaluator2.test.ts +++ b/packages/pyright-internal/src/tests/typeEvaluator2.test.ts @@ -1013,3 +1013,8 @@ test('Sentinel2', () => { const analysisResults = TestUtils.typeAnalyzeSampleFiles(['sentinel2.py'], configOptions); TestUtils.validateResults(analysisResults, 5); }); + +test('Sentinel3', () => { + const analysisResults = TestUtils.typeAnalyzeSampleFiles(['sentinel3.py']); + TestUtils.validateResults(analysisResults, 0); +});