Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 3 additions & 3 deletions packages/pyright-internal/src/analyzer/typeEvaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14656,7 +14656,7 @@ export function createTypeEvaluator(
);

tdEntries.knownItems.forEach((entry, name) => {
if (entry.isRequired || entry.isProvided) {
if (!expectedTypedDictEntries || entry.isRequired || entry.isProvided) {
keyTypes.push({
node: entryNode,
type: ClassType.cloneWithLiteral(strObject, name),
Expand All @@ -14665,11 +14665,11 @@ export function createTypeEvaluator(
}
});

if (!expectedTypedDictEntries) {
if (tdEntries.extraItems) {

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.

Issue · Please address or respond

Changing this guard makes the block run when an expected TypedDict target is present. Closed TypedDicts populate extraItems with Never, so unpacking one into an expected TypedDict now contributes a non-literal str key that assignToTypedDict rejects, producing a new assignment error. Retain the context gate while removing only the fallback: if (!expectedTypedDictEntries && tdEntries.extraItems) { ... }.

keyTypes.push({ node: entryNode, type: ClassType.cloneAsInstance(strObject) });
valueTypes.push({
node: entryNode,
type: tdEntries.extraItems?.valueType ?? getObjectType(),
type: tdEntries.extraItems.valueType,
});
}

Expand Down
39 changes: 39 additions & 0 deletions packages/pyright-internal/src/tests/samples/typedDict28.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# This sample tests dictionary expansion for TypedDicts without an expected
# TypedDict target type context.

from typing import NotRequired, TypedDict, reveal_type


class HomogeneousTD(TypedDict):
a: int
b: int


class HeterogeneousTD(TypedDict):
a: int
b: str


class OptionalTD(TypedDict):
a: int
b: NotRequired[float]


def test_homogeneous(td: HomogeneousTD):
res1 = {**td}
reveal_type(res1, expected_text="dict[str, int]")


def test_heterogeneous(td: HeterogeneousTD):
res2 = {**td}
reveal_type(res2, expected_text="dict[str, int | str]")


def test_optional(td: OptionalTD):
res3 = {**td}
reveal_type(res3, expected_text="dict[str, int | float]")


def test_multiple(td1: HomogeneousTD, td2: HeterogeneousTD, td3: OptionalTD):
res4 = {**td1, **td2, **td3}
reveal_type(res4, expected_text="dict[str, int | str | float]")

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 added cases do not cover the changed extraItems path or the expected-TypedDict context that currently regresses. Add coverage for unpacking a closed TypedDict and a PEP 728 extra_items TypedDict into a typed target, plus a non-strict inference case to cover the stated default-mode behavior.

9 changes: 9 additions & 0 deletions packages/pyright-internal/src/tests/typeEvaluator7.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,15 @@ test('TypedDict27', () => {
TestUtils.validateResults(analysisResults, 7);
});

test('TypedDict28', () => {
const configOptions = new ConfigOptions(Uri.empty());
configOptions.diagnosticRuleSet.strictDictionaryInference = true;

const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typedDict28.py'], configOptions);

TestUtils.validateResults(analysisResults, 0);
});

test('TypedDictInline1', () => {
const configOptions = new ConfigOptions(Uri.empty());
configOptions.diagnosticRuleSet.enableExperimentalFeatures = true;
Expand Down