mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-03-22 15:19:59 +07:00
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:
committed by
intellij-monorepo-bot
parent
647ae1d165
commit
b72bfa1304
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user