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 39ae4336a0c4..8c0b3263ec37 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 @@ -961,7 +961,7 @@ public final class PyTypingTypeProvider extends PyTypeProviderWithCustomContext< if (typedDictType != null) { return Ref.create(typedDictType); } - final Ref classType = getClassType(resolved, context.getTypeContext()); + final Ref 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 getClassType(@NotNull PsiElement element, @NotNull TypeEvalContext context) { + private static @Nullable Ref 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); } } diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyFunctionImpl.java b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyFunctionImpl.java index 20ecc99c4e22..68b295de1659 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyFunctionImpl.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyFunctionImpl.java @@ -223,17 +223,6 @@ public class PyFunctionImpl extends PyBaseElementImpl 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); diff --git a/python/testData/inspections/PyTypeCheckerInspection/MatchingOpenFunctionCallTypesPy3/a.py b/python/testData/inspections/PyTypeCheckerInspection/MatchingOpenFunctionCallTypesPy3/a.py index d949e5fb2b37..9559b8e3bed1 100644 --- a/python/testData/inspections/PyTypeCheckerInspection/MatchingOpenFunctionCallTypesPy3/a.py +++ b/python/testData/inspections/PyTypeCheckerInspection/MatchingOpenFunctionCallTypesPy3/a.py @@ -2,7 +2,7 @@ from foo import calcT, calcB with open('1.txt') as file1: calcT(file1) - calcB(file1) + calcB(file1) with open('1.txt', 'rb') as file2: calcT(file2) diff --git a/python/testData/intentions/PyAnnotateVariableTypeIntentionTest/annotationLocalWithTarget_after.py b/python/testData/intentions/PyAnnotateVariableTypeIntentionTest/annotationLocalWithTarget_after.py index 5faec9f6e1a1..a11dbcc538a9 100644 --- a/python/testData/intentions/PyAnnotateVariableTypeIntentionTest/annotationLocalWithTarget_after.py +++ b/python/testData/intentions/PyAnnotateVariableTypeIntentionTest/annotationLocalWithTarget_after.py @@ -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 diff --git a/python/testData/intentions/PyAnnotateVariableTypeIntentionTest/typeCommentLocalWithTargetWithExistingComment_after.py b/python/testData/intentions/PyAnnotateVariableTypeIntentionTest/typeCommentLocalWithTargetWithExistingComment_after.py index aa4db1b60b8b..9207a6ffd9a4 100644 --- a/python/testData/intentions/PyAnnotateVariableTypeIntentionTest/typeCommentLocalWithTargetWithExistingComment_after.py +++ b/python/testData/intentions/PyAnnotateVariableTypeIntentionTest/typeCommentLocalWithTargetWithExistingComment_after.py @@ -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 diff --git a/python/testData/intentions/PyAnnotateVariableTypeIntentionTest/typeCommentLocalWithTarget_after.py b/python/testData/intentions/PyAnnotateVariableTypeIntentionTest/typeCommentLocalWithTarget_after.py index 21fba74d7026..bc480b1be8a7 100644 --- a/python/testData/intentions/PyAnnotateVariableTypeIntentionTest/typeCommentLocalWithTarget_after.py +++ b/python/testData/intentions/PyAnnotateVariableTypeIntentionTest/typeCommentLocalWithTarget_after.py @@ -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 diff --git a/python/testSrc/com/jetbrains/python/Py3TypeTest.java b/python/testSrc/com/jetbrains/python/Py3TypeTest.java index b88dff1ae731..f1809b371cf3 100644 --- a/python/testSrc/com/jetbrains/python/Py3TypeTest.java +++ b/python/testSrc/com/jetbrains/python/Py3TypeTest.java @@ -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') diff --git a/python/testSrc/com/jetbrains/python/PyTypingTest.java b/python/testSrc/com/jetbrains/python/PyTypingTest.java index eec414b2cec8..9aa6f1cf3e20 100644 --- a/python/testSrc/com/jetbrains/python/PyTypingTest.java +++ b/python/testSrc/com/jetbrains/python/PyTypingTest.java @@ -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());