From e2d7d259e9a20e9afd4c9b06c3e9bb16ee034b4a Mon Sep 17 00:00:00 2001 From: Mikhail Golubev Date: Wed, 14 Aug 2024 17:43:26 +0200 Subject: [PATCH] PY-76243 Don't build implicit union types for conditional definitions and names imported from stub packages Also fixes PY-59014, PY-39761. PyResolveImportUtil returns both .pyi stubs and the corresponding .py files for stub packages to support partial stub packages. See the line: ``` groupedResults.topResultIs(Priority.STUB_PACKAGE) -> firstResultWithFallback(groupedResults, Priority.STUB_PACKAGE) ``` in PyResolveImportUtil.filterTopPriorityResults. It means that, for instance, resolving the QuerySet name in type hints led to QuerySet definitions from both places. Then, PyTypingTypeProvider.getType() for the reference expression "QuerySet" returned a union type containing PyClassTypes for both of them, we couldn't parameterize it in PyTypingTypeProvider.getParameterizedType and returned Any. It's wrong that while evaluating type hints, we interpret multiple declarations as a union type. Those should only be explicitly expressed with typing.Union or "|" operator. This behavior was originally added in PY-18427 as an ad-hoc way to support version checks for type hints, but now it seems detrimental because it's unclear how to parameterize such implicit unions of generic types then. Other type checkers also don't treat conditional definitions like that. For instance, for conditional type aliases, Mypy complains about the name being defined twice and then uses only the first definition, and Pyright doesn't consider names under conditions other than version checks as valid type aliases at all. Both type checkers also support partial stub packages properly. GitOrigin-RevId: 1ecc7ab5d09625d10850ddc0e1f7761332ccddd5 --- .../typing/PyTypingTypeProvider.java | 11 +-- .../site-packages/pkg-stubs/__init__.pyi | 0 .../site-packages/pkg-stubs/mod.pyi | 2 + .../site-packages/pkg/__init__.py | 0 .../site-packages/pkg/mod.py | 2 + .../com/jetbrains/python/PyTypingTest.java | 77 +++++++++++++++---- 6 files changed, 67 insertions(+), 25 deletions(-) create mode 100644 python/testData/types/GenericClassDeclaredInStubPackage/site-packages/pkg-stubs/__init__.pyi create mode 100644 python/testData/types/GenericClassDeclaredInStubPackage/site-packages/pkg-stubs/mod.pyi create mode 100644 python/testData/types/GenericClassDeclaredInStubPackage/site-packages/pkg/__init__.py create mode 100644 python/testData/types/GenericClassDeclaredInStubPackage/site-packages/pkg/mod.py diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.java b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.java index 57c5c231732a..8b28f3d4ea8f 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.java +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.java @@ -765,20 +765,13 @@ public final class PyTypingTypeProvider extends PyTypeProviderWithCustomContext< @Nullable private static Ref getType(@NotNull PyExpression expression, @NotNull Context context) { - final List members = new ArrayList<>(); - boolean foundAny = false; for (Pair pair : tryResolvingWithAliases(expression, context.getTypeContext())) { final Ref typeRef = getTypeForResolvedElement(expression, pair.getFirst(), pair.getSecond(), context); if (typeRef != null) { - final PyType type = typeRef.get(); - if (type == null) { - foundAny = true; - } - members.add(type); + return typeRef; } } - final PyType union = PyUnionType.union(members); - return union != null || foundAny ? Ref.create(union) : null; + return null; } @Nullable diff --git a/python/testData/types/GenericClassDeclaredInStubPackage/site-packages/pkg-stubs/__init__.pyi b/python/testData/types/GenericClassDeclaredInStubPackage/site-packages/pkg-stubs/__init__.pyi new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/types/GenericClassDeclaredInStubPackage/site-packages/pkg-stubs/mod.pyi b/python/testData/types/GenericClassDeclaredInStubPackage/site-packages/pkg-stubs/mod.pyi new file mode 100644 index 000000000000..2f6561e3a18c --- /dev/null +++ b/python/testData/types/GenericClassDeclaredInStubPackage/site-packages/pkg-stubs/mod.pyi @@ -0,0 +1,2 @@ +class MyClass[T]: + pass diff --git a/python/testData/types/GenericClassDeclaredInStubPackage/site-packages/pkg/__init__.py b/python/testData/types/GenericClassDeclaredInStubPackage/site-packages/pkg/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/types/GenericClassDeclaredInStubPackage/site-packages/pkg/mod.py b/python/testData/types/GenericClassDeclaredInStubPackage/site-packages/pkg/mod.py new file mode 100644 index 000000000000..1dea43b79188 --- /dev/null +++ b/python/testData/types/GenericClassDeclaredInStubPackage/site-packages/pkg/mod.py @@ -0,0 +1,2 @@ +class MyClass: + pass \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/PyTypingTest.java b/python/testSrc/com/jetbrains/python/PyTypingTest.java index 1899fd6a9231..c9a8081091ed 100644 --- a/python/testSrc/com/jetbrains/python/PyTypingTest.java +++ b/python/testSrc/com/jetbrains/python/PyTypingTest.java @@ -593,17 +593,55 @@ public class PyTypingTest extends PyTestCase { """); } - // PY-18427 - public void testConditionalType() { - doTest("int | str", + // PY-18427 PY-76243 + public void testConditionalTypeAlias() { + doTest("int", """ if something: Type = int else: Type = str - def f(expr: Type): - pass + expr: Type + """); + } + + public void testConditionalGenericTypeAlias() { + doTest("list[str]", + """ + if something: + Type = list + else: + Type = set + + expr: Type[str] + """); + } + + public void testTypeAliasOfUnionOfGenericTypes() { + doTest("list[str] | set[str]", + """ + from typing import TypeVar + + T = TypeVar("T") + + Type = list[T] | set[T] + + expr: Type[str] + """); + } + + public void testTypeAliasOfUnionOfGenericTypesWithDifferentArity() { + doTest("dict[str, int] | set[int]", + """ + from typing import TypeVar + + T1 = TypeVar("T1") + T2 = TypeVar("T2") + + Type = dict[T1, T2] | set[T2] + + expr: Type[str, int] """); } @@ -1950,28 +1988,24 @@ public class PyTypingTest extends PyTestCase { """ from __future__ import annotations if something: - Type = int + x: int else: - Type = str - - def f(expr: Type): - pass + x: str + expr = x """); }); } // PY-44974 - public void testWithoutFromFutureImport() { + public void testBitwiseOrUnionWithoutFromFutureImport() { runWithLanguageLevel(LanguageLevel.PYTHON39, () -> { doTest("Union[int, str]", """ if something: - Type = int + x: int else: - Type = str - - def f(expr: Type): - pass + x: str + expr = x """); }); } @@ -5787,6 +5821,17 @@ public class PyTypingTest extends PyTestCase { """); } + // PY-76243 + public void testGenericClassDeclaredInStubPackage() { + runWithAdditionalClassEntryInSdkRoots("types/" + getTestName(false) + "/site-packages", () -> { + doTest("MyClass[int]", + """ + from pkg.mod import MyClass + expr: MyClass[int] + """); + }); + } + private void doTestNoInjectedText(@NotNull String text) { myFixture.configureByText(PythonFileType.INSTANCE, text); final InjectedLanguageManager languageManager = InjectedLanguageManager.getInstance(myFixture.getProject());