PY-82454 When a generic class is not parameterized in a type hint, parameterize it with defaults right away

Previously, we parameterized it in PyReferenceExpressionImpl#getTypeFromTarget
and PyFunctionImpl#analyzeCallType, but this substitution disregarded default types
and substituted free type parameters only with their bounds if those were present,
additionally diluting them with `Any` through a "weak type".

So if we had something like the following:

```
class Ref[T : str = str]:
    def get_self(self) -> Self: ...
    def get_type_param(self) -> T: ...

x: Ref = ...
x.get_self()  # Ref[str | Any]
x.get_type_param() # str | Any
```

it worked somewhat correctly only if the omitted type parameter had a bound
in addition to the default.

One notable example from the standard library is the `open()` builtin
returning `TextIOWrapper` that has a default type parameter `_WrappedBuffer`.
This type parameter ended up either substituted with a "weak type" `_WrappedBuffer | Any`
or completely erased.

This change allowed removing special-casing for Self in PyFunctionImpl#analyzeCallType.


(cherry picked from commit 6408d24186bf607a08006f15b380e1eb158e63eb)

IJ-MR-169773

GitOrigin-RevId: 47b253ae2fe422f83b8dcfd59186433ecf55b2cc
This commit is contained in:
Mikhail Golubev
2025-07-07 12:08:44 +03:00
committed by intellij-monorepo-bot
parent 647ae1d165
commit b72bfa1304
8 changed files with 121 additions and 31 deletions

View File

@@ -961,7 +961,7 @@ public final class PyTypingTypeProvider extends PyTypeProviderWithCustomContext<
if (typedDictType != null) {
return Ref.create(typedDictType);
}
final Ref<PyType> classType = getClassType(resolved, context.getTypeContext());
final Ref<PyType> classType = getClassType(typeHint, resolved, context);
if (classType != null) {
return classType;
}
@@ -1113,12 +1113,29 @@ public final class PyTypingTypeProvider extends PyTypeProviderWithCustomContext<
return ANY.equals(getQualifiedName(element)) ? Ref.create() : null;
}
private static @Nullable Ref<PyType> getClassType(@NotNull PsiElement element, @NotNull TypeEvalContext context) {
private static @Nullable Ref<PyType> getClassType(@NotNull PyExpression typeHint, @NotNull PsiElement element, @NotNull Context context) {
if (element instanceof PyTypedElement) {
final PyType type = context.getType((PyTypedElement)element);
if (type instanceof PyClassLikeType classType) {
if (classType.isDefinition() || isNoneType(classType)) {
final PyType instanceType = classType.toInstance();
TypeEvalContext typeContext = context.getTypeContext();
final PyType type = typeContext.getType((PyTypedElement)element);
if (type instanceof PyClassLikeType classLikeType) {
if (classLikeType.isDefinition()) {
// If we're interpreting a type hint like "MyGeneric" that is not followed by a list of type arguments (e.g. MyGeneric[int]),
// we want to parameterize it with its type parameters defaults already here.
// We need this check for the type argument list because getParameterizedType() relies on getClassType() for
// getting the type corresponding to the subscription expression operand.
if (classLikeType instanceof PyClassType classType &&
isGeneric(classLikeType, typeContext) &&
!(typeHint.getParent() instanceof PySubscriptionExpression se && typeHint.equals(se.getOperand()))) {
PyCollectionType parameterized = parameterizeClassDefaultAware(classType.getPyClass(), List.of(), context);
if (parameterized != null) {
return Ref.create(parameterized.toInstance());
}
}
final PyType instanceType = classLikeType.toInstance();
return Ref.create(instanceType);
}
else if (isNoneType(classLikeType)) {
final PyType instanceType = classLikeType.toInstance();
return Ref.create(instanceType);
}
}

View File

@@ -223,17 +223,6 @@ public class PyFunctionImpl extends PyBaseElementImpl<PyFunctionStub> implements
if (PyTypeChecker.hasGenerics(type, context)) {
final var substitutions = PyTypeChecker.unifyGenericCall(receiver, parameters, context);
if (substitutions != null) {
PyClass containingClass = getContainingClass();
if (containingClass != null && type instanceof PySelfType) {
PyCollectionType genericType = PyTypeChecker.findGenericDefinitionType(containingClass, context);
if (genericType != null) {
PyClassType qualifierClassType = as(substitutions.getQualifierType(), PyClassType.class);
if (!(qualifierClassType != null &&
qualifierClassType.getPyClass().getAncestorClasses(context).contains(genericType.getPyClass()))) {
type = genericType;
}
}
}
final var substitutionsWithUnresolvedReturnGenerics =
PyTypeChecker.getSubstitutionsWithUnresolvedReturnGenerics(getParameters(context), type, substitutions, context);
type = PyTypeChecker.substitute(type, substitutionsWithUnresolvedReturnGenerics, context);

View File

@@ -2,7 +2,7 @@ from foo import calcT, calcB
with open('1.txt') as file1:
calcT(file1)
calcB(<warning descr="Expected type 'BinaryIO', got 'TextIOWrapper[_WrappedBuffer | Any]' instead">file1</warning>)
calcB(<warning descr="Expected type 'BinaryIO', got 'TextIOWrapper[_WrappedBuffer]' instead">file1</warning>)
with open('1.txt', 'rb') as file2:
calcT(<warning descr="Expected type 'TextIO', got 'BufferedReader' instead">file2</warning>)

View File

@@ -1,8 +1,7 @@
from io import TextIOWrapper, _WrappedBuffer
from typing import Union, Any
def func():
var: [TextIOWrapper[Union[_WrappedBuffer, Any]]]
var: [TextIOWrapper[_WrappedBuffer]]
with open('file.txt') as var:
var

View File

@@ -1,7 +1,6 @@
from io import TextIOWrapper, _WrappedBuffer
from typing import Union, Any
def func():
with open('file.txt') as var: # type: [TextIOWrapper[Union[_WrappedBuffer, Any]]] # comment
with open('file.txt') as var: # type: [TextIOWrapper[_WrappedBuffer]] # comment
var

View File

@@ -1,7 +1,6 @@
from io import TextIOWrapper, _WrappedBuffer
from typing import Union, Any
def func():
with open('file.txt') as var: # type: [TextIOWrapper[Union[_WrappedBuffer, Any]]]
with open('file.txt') as var: # type: [TextIOWrapper[_WrappedBuffer]]
var

View File

@@ -2,9 +2,7 @@
package com.jetbrains.python;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Ref;
import com.intellij.psi.PsiFile;
import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider;
import com.jetbrains.python.fixtures.PyTestCase;
import com.jetbrains.python.inspections.PyTypeCheckerInspectionTest;
import com.jetbrains.python.psi.LanguageLevel;
@@ -402,12 +400,12 @@ public class Py3TypeTest extends PyTestCase {
}
public void testOpenDefault() {
doTest("TextIOWrapper",
doTest("TextIOWrapper[_WrappedBuffer]",
"expr = open('foo')\n");
}
public void testOpenText() {
doTest("TextIOWrapper",
doTest("TextIOWrapper[_WrappedBuffer]",
"expr = open('foo', 'r')\n");
}
@@ -417,7 +415,7 @@ public class Py3TypeTest extends PyTestCase {
}
public void testIoOpenDefault() {
doTest("TextIOWrapper",
doTest("TextIOWrapper[_WrappedBuffer]",
"""
import io
expr = io.open('foo')
@@ -425,7 +423,7 @@ public class Py3TypeTest extends PyTestCase {
}
public void testIoOpenText() {
doTest("TextIOWrapper",
doTest("TextIOWrapper[_WrappedBuffer]",
"""
import io
expr = io.open('foo', 'r')

View File

@@ -4524,7 +4524,7 @@ public class PyTypingTest extends PyTestCase {
// PY-36444
public void testTextIOInferredWithContextManagerDecorator() {
doTest("TextIOWrapper",
doTest("TextIOWrapper[_WrappedBuffer]",
"""
from contextlib import contextmanager
@@ -6512,6 +6512,95 @@ public class PyTypingTest extends PyTestCase {
""");
}
// PY-82454
public void testMethodReturningTypeParameterCalledOnNonParameterizedGenericWithDefault() {
doTest("str", """
class Box[T=str]:
def m(self) -> T:
...
def f() -> Box:
...
expr = f().m()
""");
}
// PY-82454
public void testAttributeOfTypeParameterTypeAccessedOnNonParameterizedGenericWithDefault() {
doTest("str", """
class Box[T=str]:
attr: T
def f() -> Box:
...
expr = f().attr
""");
}
// PY-82454
public void testNonParameterizedGenericWithDefaultUsedInOtherType() {
doTest("list[Box[str]]", """
class Box[T=str]:
def m(self) -> T:
...
def f() -> list[Box]:
...
expr = f()
""");
}
// PY-82454
public void testMethodReturningSelfCalledOnNonParameterizedGenericWithDefault() {
doTest("Box[str]", """
from typing import Self
class Box[T=str]:
def m(self) -> Self:
...
def f() -> Box: # not parameterized, simulating open() -> TextIOWrapper
...
expr = f().m()
""");
}
// PY-82454
public void testMethodReturningTypeParameterizedWithSelfCalledOfNonParameterizedGenericWithDefault() {
doTest("list[Box[str]]", """
from typing import Self
class Box[T=str]:
def m(self) -> list[Self]:
...
def f() -> Box: # not parameterized, simulating open() -> TextIOWrapper
...
expr = f().m()
""");
}
// PY-82454
public void testMethodReturningSelfCalledOnNonParameterizedGenericWithDefaultAndBound() {
doTest("Box[str]", """
from typing import Self
class Box[T : str = str]:
def m(self) -> Self:
...
def f() -> Box: # not parameterized, simulating open() -> TextIOWrapper
...
expr = f().m()
""");
}
private void doTestNoInjectedText(@NotNull String text) {
myFixture.configureByText(PythonFileType.INSTANCE, text);
final InjectedLanguageManager languageManager = InjectedLanguageManager.getInstance(myFixture.getProject());