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
This commit is contained in:
Mikhail Golubev
2024-08-14 17:43:26 +02:00
committed by intellij-monorepo-bot
parent 28f446816d
commit e2d7d259e9
6 changed files with 67 additions and 25 deletions

View File

@@ -765,20 +765,13 @@ public final class PyTypingTypeProvider extends PyTypeProviderWithCustomContext<
@Nullable
private static Ref<PyType> getType(@NotNull PyExpression expression, @NotNull Context context) {
final List<PyType> members = new ArrayList<>();
boolean foundAny = false;
for (Pair<PyQualifiedNameOwner, PsiElement> pair : tryResolvingWithAliases(expression, context.getTypeContext())) {
final Ref<PyType> 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

View File

@@ -0,0 +1,2 @@
class MyClass[T]:
pass

View File

@@ -0,0 +1,2 @@
class MyClass:
pass

View File

@@ -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());