From b3b8047d22584fcf03ce1f0f4d0794dc5eea9292 Mon Sep 17 00:00:00 2001 From: Morgan Bartholomew Date: Thu, 19 Mar 2026 13:47:15 +1000 Subject: [PATCH] PY-57621 tuple types: widen for generic calls (cherry picked from commit 4b8536f547d3e86dec3e07755751640d67f01f63) GitOrigin-RevId: bb449c274a0a207e0a11eaf2112b26e458478447 --- .../stdlib/PyStdlibTypeProvider.java | 11 ++--- .../python/psi/impl/PyNamedParameterImpl.java | 3 +- .../python/psi/types/PyCollectionTypeUtil.kt | 7 ++-- .../python/psi/types/PyTypeChecker.kt | 2 +- .../jetbrains/python/psi/types/PyTypeUtil.kt | 12 ++++++ .../com/jetbrains/python/Py3TypeTest.java | 42 +++++++++++++++++++ .../com/jetbrains/python/PyTypeTest.java | 10 ++--- .../Py3TypeCheckerInspectionTest.java | 16 +++++++ .../testing/PyTestFixtureResolvingTest.kt | 2 +- 9 files changed, 86 insertions(+), 19 deletions(-) diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/stdlib/PyStdlibTypeProvider.java b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/stdlib/PyStdlibTypeProvider.java index eef647287e74..741cea30b113 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/stdlib/PyStdlibTypeProvider.java +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/stdlib/PyStdlibTypeProvider.java @@ -59,6 +59,7 @@ import java.util.stream.Stream; import static com.jetbrains.python.PyNames.TYPE_ENUM_FLAG; import static com.jetbrains.python.psi.PyUtil.as; +import static com.jetbrains.python.psi.types.PyTypeUtilKt.widenTupleLiterals; public final class PyStdlibTypeProvider extends PyTypeProviderBase { @@ -243,14 +244,8 @@ public final class PyStdlibTypeProvider extends PyTypeProviderBase { PyExpression value = targetExpression.findAssignedValue(); if (value == null) return null; - PyType type = context.getType(value); - if (type instanceof PyTupleType tupleType) { - // until heterogeneous enums are supported, we must widen tuple types - type = PyTupleType.create( - tupleType.getDeclarationElement(), - ContainerUtil.map(tupleType.getElementTypes(), t -> PyLiteralType.upcastLiteralToClass(t)) - ); - } + // until heterogeneous enums are supported, we must widen tuple types + PyType type = widenTupleLiterals(context.getType(value)); return getEnumAttributeInfo(enumClass, type, context); } else { diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyNamedParameterImpl.java b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyNamedParameterImpl.java index 07ab7bab531c..e4effacaee6a 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyNamedParameterImpl.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyNamedParameterImpl.java @@ -60,6 +60,7 @@ import com.jetbrains.python.psi.types.PyStructuralType; import com.jetbrains.python.psi.types.PyTupleType; import com.jetbrains.python.psi.types.PyType; import com.jetbrains.python.psi.types.PyTypeUtil; +import com.jetbrains.python.psi.types.PyTypeUtilKt; import com.jetbrains.python.psi.types.PyUnionType; import com.jetbrains.python.psi.types.TypeEvalContext; import one.util.streamex.StreamEx; @@ -251,7 +252,7 @@ public class PyNamedParameterImpl extends PyBaseElementImpl MAX_ANALYZED_ELEMENTS_OF_LITERALS) { PyUnionType.createWeakType(analyzedElementsType) @@ -87,8 +88,8 @@ object PyCollectionTypeUtil { .forEach { val type = context.getType(it) val (keyType, valueType) = getKeyValueType(type) - keyTypes.add(PyLiteralType.upcastLiteralToClass(keyType)) - valueTypes.add(PyLiteralType.upcastLiteralToClass(valueType)) + keyTypes.add(PyLiteralType.upcastLiteralToClass(keyType.widenTupleLiterals())) + valueTypes.add(PyLiteralType.upcastLiteralToClass(valueType.widenTupleLiterals())) } if (elements.size > MAX_ANALYZED_ELEMENTS_OF_LITERALS) { diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeChecker.kt b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeChecker.kt index daa76020e425..28a7ca74558a 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeChecker.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeChecker.kt @@ -1473,7 +1473,7 @@ object PyTypeChecker { return PyCollectionTypeImpl( genericType.pyClass, genericType.isDefinition, genericType.elementTypes.flatMap { - flattenUnpackedTuple(clone(it)) + flattenUnpackedTuple(clone(it).widenTupleLiterals()) } ) } diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeUtil.kt b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeUtil.kt index 67047b29288a..c99e478927fa 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeUtil.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeUtil.kt @@ -373,3 +373,15 @@ val PyType?.isUnknown: Boolean @ApiStatus.Internal fun PyExpression.getLiteralType(context: TypeEvalContext): PyType? = PyLiteralType.getLiteralType(this, context) + +/** + * Widens literal types within a tuple type. + * When a tuple appears nested in a non-tuple container type (e.g., `list[tuple[Literal[1], Literal["a"]]]`), + * its literal element types should be widened to their base types (e.g., `list[tuple[int, str]]`). + */ +@ApiStatus.Experimental +fun PyType?.widenTupleLiterals(): PyType? { + if (this !is PyTupleType) return this + val widenedElements = this.elementTypes.map { PyLiteralType.upcastLiteralToClass(it.widenTupleLiterals()) } + return PyTupleType(this.pyClass, widenedElements, this.isHomogeneous) +} \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/Py3TypeTest.java b/python/testSrc/com/jetbrains/python/Py3TypeTest.java index 88593b8cddca..35b9351b221b 100644 --- a/python/testSrc/com/jetbrains/python/Py3TypeTest.java +++ b/python/testSrc/com/jetbrains/python/Py3TypeTest.java @@ -5262,6 +5262,48 @@ public class Py3TypeTest extends PyTestCase { """); } + @TestFor(issues="PY-57621") + public void testTupleInListWidens() { + doTest("list[tuple[int, str]]", """ + t = (1, 'hello') + expr = [t] + """); + } + + @TestFor(issues="PY-57621") + public void testTupleInTupleIsLiteral() { + var t = "tuple[Literal[1], Literal['hello']]"; + doTest("tuple[" + t + ", " + t + "]", """ + t = (1, 'hello') + expr = (t, t) + """); + } + + @TestFor(issues="PY-57621") + public void testTupleInGenericWidens() { + doTest("list[tuple[int, str]]", """ + def f[T](t: T) -> list[T]: ... + expr = f((1, "hello")) + """); + } + + @TestFor(issues="PY-57621") + public void testTupleAsGenericInTupleNarrows() { + var t = "tuple[Literal[1], Literal['hello']]"; + doTest("tuple[list[tuple[int, str]], " + t + "]" + " | " + t, """ + def f[T](t: T) -> tuple[list[T], T] | T: ... + expr = f((1, 'hello')) + """); + } + + @TestFor(issues="PY-57621") + public void testTupleAsBareTypeVariableIsLiteral() { + doTest("tuple[Literal[1], Literal[\"hello\"]]", """ + def f[T](t: T) -> T: ... + expr = f((1, "hello")) + """); + } + private void doTest(final String expectedType, final String text) { myFixture.configureByText(PythonFileType.INSTANCE, text); final PyExpression expr = myFixture.findElementByText("expr", PyExpression.class); diff --git a/python/testSrc/com/jetbrains/python/PyTypeTest.java b/python/testSrc/com/jetbrains/python/PyTypeTest.java index b48c1feaad3c..a3c845c84e9b 100644 --- a/python/testSrc/com/jetbrains/python/PyTypeTest.java +++ b/python/testSrc/com/jetbrains/python/PyTypeTest.java @@ -1002,7 +1002,7 @@ public class PyTypeTest extends PyTestCase { // PY-9334 public void testIterateOverListOfNestedTuples() { - doTest("Literal['foo']", + doTest("str", """ def f(): for i, (expr, v) in [(0, ('foo', []))]: @@ -1047,7 +1047,7 @@ public class PyTypeTest extends PyTestCase { // PY-10967 public void testDefaultTupleParameterMember() { - doTest("Literal[1]", + doTest("int", """ def foo(xs=(1, 2)): expr, foo = xs @@ -1174,7 +1174,7 @@ public class PyTypeTest extends PyTestCase { // PY-38928 public void testIterateListOfTuples() { doTest( - "Literal['foo']", + "str", """ for ((_, expr)) in [(1, 'foo')]: pass @@ -2721,7 +2721,7 @@ public class PyTypeTest extends PyTestCase { } public void testUnpackingToNestedTargetsInSquareBracketsInForLoops() { - doTest("Literal[\"foo\"]", + doTest("str", """ xs = [(1, ("foo",))] for [_, [expr]] in xs: @@ -2730,7 +2730,7 @@ public class PyTypeTest extends PyTestCase { } public void testUnpackingToNestedTargetsInSquareBracketsInComprehensions() { - doTest("Literal[\"foo\"]", + doTest("str", """ xs = [(1, ("foo",))] ys = [expr for [_, [expr]] in xs] diff --git a/python/testSrc/com/jetbrains/python/inspections/Py3TypeCheckerInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/Py3TypeCheckerInspectionTest.java index a61b59e33f79..e639805382e0 100644 --- a/python/testSrc/com/jetbrains/python/inspections/Py3TypeCheckerInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/Py3TypeCheckerInspectionTest.java @@ -1,6 +1,7 @@ // Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.jetbrains.python.inspections; +import com.intellij.idea.TestFor; import com.intellij.openapi.util.RecursionManager; import com.intellij.openapi.util.StackOverflowPreventedException; import com.jetbrains.python.fixtures.PyInspectionTestCase; @@ -4371,5 +4372,20 @@ public class Py3TypeCheckerInspectionTest extends PyInspectionTestCase { return set(self) # OK """); } + + @TestFor(issues="PY-57621") + public void testTupleInGenericExplicitIsValid() { + doTestByText(""" + from typing import Literal + + class A[T]: + def __init__(self, t: T): ... + + A[list[tuple[Literal[1]]]]([(1,)]) + + _: list[tuple[Literal[1]]] = [(1,)] + _: list[tuple[int]] = [(1,)] + """); + } } diff --git a/python/testSrc/com/jetbrains/python/testing/PyTestFixtureResolvingTest.kt b/python/testSrc/com/jetbrains/python/testing/PyTestFixtureResolvingTest.kt index a59dad40f3d0..437a228ea040 100644 --- a/python/testSrc/com/jetbrains/python/testing/PyTestFixtureResolvingTest.kt +++ b/python/testSrc/com/jetbrains/python/testing/PyTestFixtureResolvingTest.kt @@ -361,7 +361,7 @@ class PyTestFixtureResolvingTest : PyTestCase() { } fun testNamedParameterTypes() { - assertCorrectType(PARAMETRIZED_DIR, TEST_PARAMETER_TYPES, "Literal[9] | str") + assertCorrectType(PARAMETRIZED_DIR, TEST_PARAMETER_TYPES, INT_STR_UNION) } @TestFor(issues = ["PY-56268"])