Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion packages/pyright-internal/src/analyzer/typeEvaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning · Non-blocking recommendation

The new sample exercises module-level names but not the member-access widening path modified here. Add a class or instance sentinel-member regression case so this distinct retention path is covered.

Expand Down Expand Up @@ -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)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning · Non-blocking recommendation

📍 packages/pyright-internal/src/analyzer/typeEvaluator.ts:24658
[unverified] The corresponding Pylance async-evaluator widening branches do not contain this exception, so IDE behavior may diverge when async evaluation is enabled. Verify the scenario with a downstream async-mode Pylance regression test and mirror the evaluator change only if that path owns the behavior rather than TSP.

[verified]

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.
Expand Down
44 changes: 44 additions & 0 deletions packages/pyright-internal/src/tests/samples/sentinel3.py
Original file line number Diff line number Diff line change
@@ -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")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning · Non-blocking recommendation

📍 packages/pyright-internal/src/tests/samples/sentinel3.py:8
[unverified] The new policy relies on singleton identity while this non-Final binding remains mutable. Add adversarial coverage for reassignment and aliasing—and preferably lowercase local and module-attribute bindings—to establish that narrowing remains sound across the affected flow paths.

[verified]

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")
5 changes: 5 additions & 0 deletions packages/pyright-internal/src/tests/typeEvaluator2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});