diff --git a/packages/pyright-internal/src/analyzer/typeEvaluator.ts b/packages/pyright-internal/src/analyzer/typeEvaluator.ts index 7f759ee2af06..086524072aa3 100644 --- a/packages/pyright-internal/src/analyzer/typeEvaluator.ts +++ b/packages/pyright-internal/src/analyzer/typeEvaluator.ts @@ -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), @@ -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, }); } diff --git a/packages/pyright-internal/src/tests/fourslash/hover.typedDictUnpacking.dictDisplay.fourslash.ts b/packages/pyright-internal/src/tests/fourslash/hover.typedDictUnpacking.dictDisplay.fourslash.ts new file mode 100644 index 000000000000..39bef71fcbc2 --- /dev/null +++ b/packages/pyright-internal/src/tests/fourslash/hover.typedDictUnpacking.dictDisplay.fourslash.ts @@ -0,0 +1,15 @@ +/// + +// @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```', +}); diff --git a/packages/pyright-internal/src/tests/samples/typedDict28.py b/packages/pyright-internal/src/tests/samples/typedDict28.py new file mode 100644 index 000000000000..190a5a65878d --- /dev/null +++ b/packages/pyright-internal/src/tests/samples/typedDict28.py @@ -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]") diff --git a/packages/pyright-internal/src/tests/samples/typedDict29.py b/packages/pyright-internal/src/tests/samples/typedDict29.py new file mode 100644 index 000000000000..e5613b5bd89d --- /dev/null +++ b/packages/pyright-internal/src/tests/samples/typedDict29.py @@ -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]") diff --git a/packages/pyright-internal/src/tests/typeEvaluator7.test.ts b/packages/pyright-internal/src/tests/typeEvaluator7.test.ts index 0776c53fb944..f28ef59c72f3 100644 --- a/packages/pyright-internal/src/tests/typeEvaluator7.test.ts +++ b/packages/pyright-internal/src/tests/typeEvaluator7.test.ts @@ -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;