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
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 (!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
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/// <reference path="typings/fourslash.d.ts" />

// @filename: test.py
//// from typing import TypedDict
////
//// class User(TypedDict):
//// name: str
//// age: int
////
//// user: User = {"name": "Alice", "age": 30}
//// [|/*marker1*/res|] = {**user}

helper.verifyHover('markdown', {
marker1: '```python\n(variable) res: dict[str, Unknown]\n```',
});
68 changes: 68 additions & 0 deletions packages/pyright-internal/src/tests/samples/typedDict28.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# This sample tests dictionary expansion for TypedDicts without an expected
# TypedDict target type context, as well as unpacking into typed targets.

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]


class ClosedTD(TypedDict, closed=True):
a: int


class TargetTD(TypedDict):
a: int


class ExtraItemsTD(TypedDict, extra_items=str):
a: int


class TargetExtraTD(TypedDict, extra_items=str):
a: int


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


def test_heterogeneous_default_mode(td: HeterogeneousTD):
# In default mode (without strictDictionaryInference), heterogeneous value types
# fall back to Unknown.
res2 = {**td}
reveal_type(res2, expected_text="dict[str, Unknown]")


def test_optional_default_mode(td: OptionalTD):
res3 = {**td}
reveal_type(res3, expected_text="dict[str, Unknown]")


def test_unpack_closed_into_target(td: ClosedTD):
# Unpacking a closed TypedDict into a typed target should not produce assignment errors.
target: TargetTD = {**td}


def test_unpack_extra_items_into_target(td: ExtraItemsTD):
target: TargetExtraTD = {**td}


def test_unpack_extra_items_into_dict(td: ExtraItemsTD):
res5 = {**td}
# Unpacking extra_items=str TypedDict into dict display under strictDictionaryInference (or combineTypes)
# includes extraItems valueType str. In default mode, int | str falls back to Unknown.
reveal_type(res5, expected_text="dict[str, Unknown]")
42 changes: 42 additions & 0 deletions packages/pyright-internal/src/tests/samples/typedDict29.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# This sample tests dictionary expansion for TypedDicts when strictDictionaryInference is enabled.

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]


class ExtraItemsTD(TypedDict, extra_items=str):
a: int


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


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


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


def test_extra_items_strict(td: ExtraItemsTD):
res4 = {**td}
reveal_type(res4, expected_text="dict[str, int | str]")
19 changes: 19 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,25 @@ test('TypedDict27', () => {
TestUtils.validateResults(analysisResults, 7);
});

test('TypedDict28', () => {
const configOptions = new ConfigOptions(Uri.empty());
configOptions.defaultPythonVersion = pythonVersion3_13;

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

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

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

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

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

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