diff --git a/python/helpers/typeshed/stdlib/typing.pyi b/python/helpers/typeshed/stdlib/typing.pyi index a3ee56dad9fa..dabb4c0fe02e 100644 --- a/python/helpers/typeshed/stdlib/typing.pyi +++ b/python/helpers/typeshed/stdlib/typing.pyi @@ -195,11 +195,11 @@ if sys.version_info >= (3, 11): LiteralString: _SpecialForm class TypeVarTuple: - __name__: str - def __init__(self, name: str) -> None: ... - def __iter__(self) -> Any: ... - def __typing_subst__(self, arg: Never) -> Never: ... - def __typing_prepare_subst__(self, alias: Incomplete, args: Incomplete) -> Incomplete: ... + __name__: str + def __init__(self, name: str) -> None: ... + def __iter__(self) -> Any: ... + def __typing_subst__(self, arg: Never) -> Never: ... + def __typing_prepare_subst__(self, alias: Incomplete, args: Incomplete) -> Incomplete: ... if sys.version_info >= (3, 10): class ParamSpecArgs: diff --git a/python/python-psi-api/src/com/jetbrains/python/psi/PyCallExpression.java b/python/python-psi-api/src/com/jetbrains/python/psi/PyCallExpression.java index 8096d520c813..bec044c03266 100644 --- a/python/python-psi-api/src/com/jetbrains/python/psi/PyCallExpression.java +++ b/python/python-psi-api/src/com/jetbrains/python/psi/PyCallExpression.java @@ -173,6 +173,7 @@ public interface PyCallExpression extends PyCallSiteExpression { @NotNull private final List myImplicitParameters; @NotNull private final Map myMappedParameters; @NotNull private final List myUnmappedParameters; + @NotNull private final List myUnmappedContainerParameters; @NotNull private final List myUnmappedArguments; @NotNull private final List myParametersMappedToVariadicPositionalArguments; @NotNull private final List myParametersMappedToVariadicKeywordArguments; @@ -183,6 +184,7 @@ public interface PyCallExpression extends PyCallSiteExpression { @NotNull List implicitParameters, @NotNull Map mappedParameters, @NotNull List unmappedParameters, + @NotNull List unmappedContainerParameters, @NotNull List unmappedArguments, @NotNull List parametersMappedToVariadicPositionalArguments, @NotNull List parametersMappedToVariadicKeywordArguments, @@ -192,6 +194,7 @@ public interface PyCallExpression extends PyCallSiteExpression { myImplicitParameters = implicitParameters; myMappedParameters = mappedParameters; myUnmappedParameters = unmappedParameters; + myUnmappedContainerParameters = unmappedContainerParameters; myUnmappedArguments = unmappedArguments; myParametersMappedToVariadicPositionalArguments = parametersMappedToVariadicPositionalArguments; myParametersMappedToVariadicKeywordArguments = parametersMappedToVariadicKeywordArguments; @@ -208,6 +211,7 @@ public interface PyCallExpression extends PyCallSiteExpression { Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), + Collections.emptyList(), Collections.emptyMap()); } @@ -236,6 +240,11 @@ public interface PyCallExpression extends PyCallSiteExpression { return myUnmappedParameters; } + @NotNull + public List getUnmappedContainerParameters() { + return myUnmappedContainerParameters; + } + @NotNull public List getUnmappedArguments() { return myUnmappedArguments; diff --git a/python/python-psi-impl/resources/messages/PyPsiBundle.properties b/python/python-psi-impl/resources/messages/PyPsiBundle.properties index 14fb984df6bc..092b1064f9be 100644 --- a/python/python-psi-impl/resources/messages/PyPsiBundle.properties +++ b/python/python-psi-impl/resources/messages/PyPsiBundle.properties @@ -475,6 +475,9 @@ INSP.NAME.pytest.unpassed.fixture=Fixture is not requested by test functions INSP.use.fixture.without.declaration.in.test.function=Fixture ''{0}'' is not requested by test functions or '@pytest.mark.usefixtures' marker QFIX.add.fixture.to.test.function.parameters.list=Add fixture to test function parameters +# PyReplaceStarByUnpackQuickFix +QFIX.replace.star.by.unpack=Replace by typing_extensions.Unpack + # Actions and associated commands ACT.CMD.use.import=Use an imported module @@ -1055,6 +1058,8 @@ INSP.type.checker.type.does.not.have.expected.attribute=Type ''{0}'' doesn''t ha INSP.type.checker.only.concrete.class.can.be.used.where.matched.protocol.expected=Only a concrete class can be used where ''{0}'' (matched generic type ''{1}'') protocol is expected INSP.type.checker.only.concrete.class.can.be.used.where.protocol.expected=Only a concrete class can be used where ''{0}'' protocol is expected INSP.type.checker.expected.matched.type.got.type.instead=Expected type ''{0}'' (matched generic type ''{1}''), got ''{2}'' instead +INSP.type.checker.no.expected.type.for=The type is not expected +INSP.type.checker.no.arguments.for.expected.type=No arguments for types (''{0}'') INSP.type.checker.unexpected.types.prefix=Unexpected type(s): INSP.type.checker.expected.types.prefix=Possible type(s): INSP.type.checker.unmapped.argument.with.paramspec=Unexpected argument (from ParamSpec ''{0}'') @@ -1094,8 +1099,10 @@ INSP.type.hints.type.specified.both.in.type.comment.and.annotation=Types specifi INSP.type.hints.typevar.expression.must.be.always.directly.assigned.to.variable=A 'TypeVar()' expression must always directly be assigned to a variable INSP.type.hints.type.variables.must.not.be.redefined=Type variables must not be redefined INSP.type.hints.typevar.expects.string.literal.as.first.argument='TypeVar()' expects a string literal as first argument +INSP.type.hints.typevar.tuple.expects.string.literal.as.first.argument='TypeVarTuple()' expects a string literal as first argument INSP.type.hints.paramspec.expects.string.literal.as.first.argument='ParamSpec()' expects a string literal as first argument INSP.type.hints.argument.to.typevar.must.be.string.equal.to.variable.name=The argument to 'TypeVar()' must be a string equal to the variable name to which it is assigned +INSP.type.hints.argument.to.typevar.tuple.must.be.string.equal.to.variable.name=The argument to 'TypeVarTuple()' must be a string equal to the variable name to which it is assigned INSP.type.hints.argument.to.paramspec.must.be.string.equal.to.variable.name=The argument to 'ParamSpec()' must be a string equal to the variable name to which it is assigned INSP.type.hints.bivariant.type.variables.are.not.supported=Bivariant type variables are not supported INSP.type.hints.typevar.constraints.cannot.be.combined.with.bound=Constraints cannot be combined with bound=\u2026 @@ -1114,6 +1121,7 @@ INSP.type.hints.parameters.to.generic.must.all.be.unique=Parameters to 'Generic[ INSP.type.hints.illegal.callable.format='Callable' must be used as 'Callable[[arg, ...], result]' INSP.type.hints.illegal.first.parameter='Callable' first parameter must be a parameter expression INSP.type.hints.parameters.to.generic.types.must.be.types=Parameters to generic types must be types +INSP.type.hints.parameters.to.generic.types.cannot.contain.more.than.one.unpacking=Parameters to generic cannot contain more than one unpacking INSP.type.hints.type.comment.cannot.be.matched.with.unpacked.variables=Type comment cannot be matched with unpacked variables INSP.type.hints.type.signature.has.too.few.arguments=Type signature has too few arguments INSP.type.hints.type.signature.has.too.many.arguments=Type signature has too many arguments @@ -1127,6 +1135,7 @@ INSP.type.hints.self.use.in.staticmethod=Cannot use 'Self' in staticmethod INSP.type.hints.self.use.outside.class=Cannot use 'Self' outside class INSP.type.hints.self.use.for.self.parameter.with.self.annotation=Cannot use 'Self' if 'self' parameter is not 'Self' annotated INSP.type.hints.self.use.for.cls.parameter.with.self.annotation=Cannot use 'Self' if 'cls' parameter is not 'Self' annotated +INSP.type.hints.type.var.tuple.must.always.be.unpacked=TypeVarTuple must always be unpacked QFIX.remove.function.annotations=Remove function annotations QFIX.replace.with.target.name=Replace with the target name QFIX.remove.generic.parameters=Remove generic parameters @@ -1139,6 +1148,7 @@ QFIX.remove.annotation=Remove the annotation QFIX.replace.with.type.name=Replace with the type name QFIX.replace.with.old.union.style=Replace with an old-style Union QFIX.add.from.future.import.annotations=Add 'from __future__ import annotations' +QFIX.NAME.unpack.type.var.tuple=Unpack the TypeVarTuple # PyInspectionsSuppressor INSP.python.suppressor.suppress.for.function=Suppress for a function 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 aeb885ad8ccf..f7bebdaf538f 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 @@ -84,6 +84,7 @@ public class PyTypingTypeProvider extends PyTypeProviderWithCustomContext { PyTypeChecker.Generics typeParams = PyTypeChecker.collectGenerics(type, context.myContext); - return StreamEx.of(typeParams.getTypeVars()).append(StreamEx.of(typeParams.getParamSpecs())); + return StreamEx.of(typeParams.getTypeVars()).append(typeParams.getTypeVarTuples()).append(StreamEx.of(typeParams.getParamSpecs())); }) .select(PyTypeParameterType.class) .distinct() @@ -847,6 +850,10 @@ public class PyTypingTypeProvider extends PyTypeProviderWithCustomContext calleeQNames = resolveToQualifiedNames(callee, context.getTypeContext()); - if (calleeQNames.contains(TYPE_VAR)) { + if (calleeQNames.contains(TYPE_VAR) || calleeQNames.contains(TYPE_VAR_TUPLE)) { final PyExpression[] arguments = assignedCall.getArguments(); if (arguments.length > 0) { final PyExpression firstArgument = arguments[0]; if (firstArgument instanceof PyStringLiteralExpression) { final String name = ((PyStringLiteralExpression)firstArgument).getStringValue(); - return new PyTypeVarTypeImpl(name, getGenericTypeBound(arguments, context)); + if (calleeQNames.contains(TYPE_VAR_TUPLE)) { + return new PyGenericVariadicType(name); + } + else { + return new PyTypeVarTypeImpl(name, getGenericTypeBound(arguments, context)); + } } } } @@ -1546,6 +1558,26 @@ public class PyTypingTypeProvider extends PyTypeProviderWithCustomContext unmatchedParameters = new ArrayList<>(); final var receiver = callSite.getReceiver(callableType.getCallable()); - final var substitutions = PyTypeChecker.unifyReceiverWithParamSpecs(receiver, myTypeEvalContext); + final var substitutions = PyTypeChecker.unifyReceiver(receiver, myTypeEvalContext); final var mappedParameters = mapping.getMappedParameters(); final var regularMappedParameters = getRegularMappedParameters(mappedParameters); @@ -354,6 +355,18 @@ public class PyTypeCheckerInspection extends PyInspection { } } + for (var unmappedContainer: mapping.getUnmappedContainerParameters()) { + PyType containerType = unmappedContainer.getArgumentType(myTypeEvalContext); + if (!(containerType instanceof PyGenericVariadicType)) continue; + var matchingResult = new ArrayList>(); + var matchContext = PyTypeChecker.getMatchContext(myTypeEvalContext, substitutions); + if (!PyTypeChecker.matchElementTypes(List.of(containerType), Collections.emptyList(), matchContext, true, false, + false, matchingResult)) { + PyType substContainerType = PyTypeChecker.substitute(containerType, substitutions, myTypeEvalContext); + result.add(new AnalyzeArgumentResult(callSite, containerType, substContainerType, PyNotMatchedType.INSTANCE, false)); + } + } + return new AnalyzeCalleeResults(callableType, callableType.getCallable(), result, unmatchedArguments, unmatchedParameters); } @@ -404,13 +417,55 @@ public class PyTypeCheckerInspection extends PyInspection { @NotNull List arguments, @NotNull PyTypeChecker.GenericSubstitutions substitutions) { final PyType expected = container.getArgumentType(myTypeEvalContext); + + if (expected instanceof PyGenericVariadicType) { + var matchContext = PyTypeChecker.getMatchContext(myTypeEvalContext, substitutions); + + var expectedElementTypes = List.of(expected); + var actualElementTypes = ContainerUtil.map(arguments, it -> myTypeEvalContext.getType(it)); + + var matchingResult = new ArrayList>(); + PyTypeChecker.matchElementTypes(expectedElementTypes, actualElementTypes, matchContext, true, false, + false, matchingResult); + + var result = new ArrayList(); + for (int i = 0; i < actualElementTypes.size(); ++i) { + if (i >= matchingResult.size()) break; + var res = matchingResult.get(i); + boolean matched = res.third; + PyType actualType = res.first; + PyType expectedType = res.second; + PyType substitutedExpectedType = substituteGenerics(expectedType, substitutions); + result.add(new AnalyzeArgumentResult(arguments.get(i), expectedType, substitutedExpectedType, actualType, matched)); + } + if (matchingResult.size() > actualElementTypes.size()) { + PsiElement lastArgNextSibling = arguments.get(arguments.size() - 1).getNextSibling(); + var lastMatingRes = matchingResult.get(matchingResult.size() - 1); + boolean matched = lastMatingRes.third; + PyType actualType = lastMatingRes.first; + PyType expectedType = lastMatingRes.second; + PyType substitutedExpectedType = substituteGenerics(expectedType, substitutions); + result.add(new AnalyzeArgumentResult(lastArgNextSibling, expectedType, substitutedExpectedType, actualType, matched)); + } + + return result; + } + final PyType expectedWithSubstitutions = substituteGenerics(expected, substitutions); // For an expected type with generics we have to match all the actual types against it in order to do proper generic unification if (PyTypeChecker.hasGenerics(expected, myTypeEvalContext)) { - final PyType actual = PyUnionType.union(ContainerUtil.map(arguments, myTypeEvalContext::getType)); - final boolean matched = matchParameterAndArgument(expected, actual, null, substitutions); - return ContainerUtil.map(arguments, argument -> - new AnalyzeArgumentResult(argument, expected, expectedWithSubstitutions, actual, matched)); + if (container.isPositionalContainer()) { + PyType actual = PyGenericVariadicType.fromElementTypes(ContainerUtil.map(arguments, myTypeEvalContext::getType)); + boolean matched = matchParameterAndArgument(PyGenericVariadicType.homogeneous(expected), actual, null, substitutions); + return ContainerUtil.map(arguments, argument -> + new AnalyzeArgumentResult(argument, expected, expectedWithSubstitutions, actual, matched)); + } + else { + PyType actual = PyUnionType.union(ContainerUtil.map(arguments, myTypeEvalContext::getType)); + boolean matched = matchParameterAndArgument(expected, actual, null, substitutions); + return ContainerUtil.map(arguments, argument -> + new AnalyzeArgumentResult(argument, expected, expectedWithSubstitutions, actual, matched)); + } } else { return ContainerUtil.map( @@ -542,7 +597,7 @@ public class PyTypeCheckerInspection extends PyInspection { static class AnalyzeArgumentResult { @NotNull - private final PyExpression myArgument; + private final PsiElement myArgument; @Nullable private final PyType myExpectedType; @@ -555,7 +610,7 @@ public class PyTypeCheckerInspection extends PyInspection { private final boolean myIsMatched; - AnalyzeArgumentResult(@NotNull PyExpression argument, + AnalyzeArgumentResult(@NotNull PsiElement argument, @Nullable PyType expectedType, @Nullable PyType expectedTypeAfterSubstitution, @Nullable PyType actualType, @@ -568,7 +623,7 @@ public class PyTypeCheckerInspection extends PyInspection { } @NotNull - public PyExpression getArgument() { + public PsiElement getArgument() { return myArgument; } diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypeCheckerInspectionProblemRegistrar.java b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypeCheckerInspectionProblemRegistrar.java index b3068eeb4228..eb5f20292eb5 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypeCheckerInspectionProblemRegistrar.java +++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypeCheckerInspectionProblemRegistrar.java @@ -31,10 +31,7 @@ import com.jetbrains.python.codeInsight.typing.PyProtocolsKt; import com.jetbrains.python.documentation.PythonDocumentationProvider; import com.jetbrains.python.psi.*; import com.jetbrains.python.psi.impl.PyPsiUtils; -import com.jetbrains.python.psi.types.PyClassLikeType; -import com.jetbrains.python.psi.types.PyStructuralType; -import com.jetbrains.python.psi.types.PyType; -import com.jetbrains.python.psi.types.TypeEvalContext; +import com.jetbrains.python.psi.types.*; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -112,6 +109,14 @@ final class PyTypeCheckerInspectionProblemRegistrar { final PyType actualType = argumentResult.getActualType(); final PyType expectedType = argumentResult.getExpectedType(); + if (expectedType instanceof PyNotMatchedType) { + return PyPsiBundle.message("INSP.type.checker.no.expected.type.for"); + } + if (expectedType instanceof PyGenericVariadicType && actualType instanceof PyNotMatchedType) { + return PyPsiBundle.message("INSP.type.checker.no.arguments.for.expected.type", + ((PyGenericVariadicType)expectedType).getElementTypesToStr()); + } + assert actualType != null; // see PyTypeCheckerInspection.Visitor.analyzeArgument() assert expectedType != null; // see PyTypeCheckerInspection.Visitor.analyzeArgument() diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypeHintsInspection.kt b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypeHintsInspection.kt index 2e49419714e2..b92577a84a0f 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypeHintsInspection.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypeHintsInspection.kt @@ -27,6 +27,7 @@ import com.jetbrains.python.codeInsight.typeHints.PyTypeHintFile import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider.isBitwiseOrUnionAvailable import com.jetbrains.python.documentation.PythonDocumentationProvider +import com.jetbrains.python.inspections.quickfix.PyUnpackTypeVarTupleQuickFix import com.jetbrains.python.psi.* import com.jetbrains.python.psi.impl.PyBuiltinCache import com.jetbrains.python.psi.impl.PyEvaluator @@ -61,8 +62,11 @@ class PyTypeHintsInspection : PyInspection() { } if (QualifiedName.fromDottedString(PyTypingTypeProvider.TYPING_PARAM_SPEC) in calleeQName) { - val target = getTargetFromAssignment(node) - checkParamSpecArguments(node, target) + checkParamSpecArguments(node, getTargetFromAssignment(node)) + } + + if (QualifiedName.fromDottedString(PyTypingTypeProvider.TYPE_VAR_TUPLE) in calleeQName) { + checkTypeVarTupleArguments(node, getTargetFromAssignment(node)) } checkInstanceAndClassChecks(node) @@ -107,6 +111,17 @@ class PyTypeHintsInspection : PyInspection() { } } + private fun checkTypeVarTupleUnpacked(node: PyReferenceExpression) { + if (PsiTreeUtil.getParentOfType(node, PyStarExpression::class.java) != null) return + val subscriptionExpr = PsiTreeUtil.getParentOfType(node, PySubscriptionExpression::class.java) + if (subscriptionExpr != null && subscriptionExpr.operand is PyQualifiedExpression && + (subscriptionExpr.operand as PyQualifiedExpression).asQualifiedName()?.endsWith("Unpack") == true) { + return + } + holder?.registerProblem(node, PyPsiBundle.message("INSP.type.hints.type.var.tuple.must.always.be.unpacked"), + PyUnpackTypeVarTupleQuickFix()) + } + override fun visitPyReferenceExpression(node: PyReferenceExpression) { super.visitPyReferenceExpression(node) @@ -114,6 +129,10 @@ class PyTypeHintsInspection : PyInspection() { return } + if (myTypeEvalContext.getType(node) is PyGenericVariadicType) { + checkTypeVarTupleUnpacked(node) + } + if (node.referencedName == PyNames.CANONICAL_SELF) { val typeName = myTypeEvalContext.getType(node)?.name if (typeName != null && typeName != PyNames.CANONICAL_SELF) { @@ -277,6 +296,16 @@ class PyTypeHintsInspection : PyInspection() { } } + private fun checkTypeVarTupleArguments(call: PyCallExpression, target: PyExpression?) { + processMatchedArgument(call) { name, argument -> + if (name == "name") { + checkNameIsTheSameAsTarget(argument, target, + PyPsiBundle.message("INSP.type.hints.typevar.tuple.expects.string.literal.as.first.argument"), + PyPsiBundle.message("INSP.type.hints.argument.to.typevar.tuple.must.be.string.equal.to.variable.name")) + } + } + } + private fun checkTypeVarArguments(call: PyCallExpression, target: PyExpression?) { var covariant = false var contravariant = false @@ -708,7 +737,7 @@ class PyTypeHintsInspection : PyInspection() { val genericParameters = mutableSetOf() parameters.forEach { - if (it !is PyReferenceExpression) { + if (it !is PyReferenceExpression && it !is PyStarExpression && it !is PySubscriptionExpression) { registerProblem(it, PyPsiBundle.message("INSP.type.hints.parameters.to.generic.must.all.be.type.variables"), ProblemHighlightType.GENERIC_ERROR) } @@ -717,7 +746,7 @@ class PyTypeHintsInspection : PyInspection() { if (type != null) { if (type is PyGenericType || isParamSpecOrConcatenate(it, myTypeEvalContext)) { - if (!genericParameters.addAll(multiFollowAssignmentsChain(it))) { + if (it is PyReferenceExpression && !genericParameters.addAll(multiFollowAssignmentsChain(it))) { registerProblem(it, PyPsiBundle.message("INSP.type.hints.parameters.to.generic.must.all.be.unique"), ProblemHighlightType.GENERIC_ERROR) } @@ -775,6 +804,7 @@ class PyTypeHintsInspection : PyInspection() { private fun checkTypingMemberParameters(index: PyExpression, isCallable: Boolean) { val parameters = if (index is PyTupleExpression) index.elements else arrayOf(index) + var alreadyHaveUnpacking = false parameters .asSequence() .drop(if (isCallable) 1 else 0) @@ -790,6 +820,15 @@ class PyTypeHintsInspection : PyInspection() { registerProblem(it, PyPsiBundle.message("INSP.type.hints.parameters.to.generic.types.must.be.types"), ProblemHighlightType.GENERIC_ERROR) } + else if (it is PyStarExpression) { + if (alreadyHaveUnpacking) { + registerProblem(it, PyPsiBundle.message("INSP.type.hints.parameters.to.generic.types.cannot.contain.more.than.one.unpacking"), + ProblemHighlightType.GENERIC_ERROR) + } + else { + alreadyHaveUnpacking = true + } + } } } diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/quickfix/PyReplaceStarByUnpackQuickFix.java b/python/python-psi-impl/src/com/jetbrains/python/inspections/quickfix/PyReplaceStarByUnpackQuickFix.java new file mode 100644 index 000000000000..b473d54636ae --- /dev/null +++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/quickfix/PyReplaceStarByUnpackQuickFix.java @@ -0,0 +1,32 @@ +package com.jetbrains.python.inspections.quickfix; + +import com.intellij.codeInspection.LocalQuickFix; +import com.intellij.codeInspection.ProblemDescriptor; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.jetbrains.python.PyPsiBundle; +import com.jetbrains.python.psi.PyExpression; +import com.jetbrains.python.psi.PyStarExpression; +import org.jetbrains.annotations.NotNull; + +public class PyReplaceStarByUnpackQuickFix implements LocalQuickFix { + @NotNull + @Override + public String getFamilyName() { + return PyPsiBundle.message("QFIX.replace.star.by.unpack"); + } + + @Override + public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { + PsiElement element = descriptor.getPsiElement(); + if (!(element instanceof PyStarExpression)) return; + + PyStarExpression starExpression = (PyStarExpression)element; + PyExpression expression = starExpression.getExpression(); + if (expression == null) return; + PsiFile file = starExpression.getContainingFile(); + + PyUnpackTypeVarTupleQuickFix.replaceToTypingExtensionsUnpack(starExpression, expression, file, project); + } +} diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/quickfix/PyUnpackTypeVarTupleQuickFix.java b/python/python-psi-impl/src/com/jetbrains/python/inspections/quickfix/PyUnpackTypeVarTupleQuickFix.java new file mode 100644 index 000000000000..fd63c45ecd0c --- /dev/null +++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/quickfix/PyUnpackTypeVarTupleQuickFix.java @@ -0,0 +1,55 @@ +package com.jetbrains.python.inspections.quickfix; + +import com.intellij.codeInspection.LocalQuickFix; +import com.intellij.codeInspection.ProblemDescriptor; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.jetbrains.python.PyPsiBundle; +import com.jetbrains.python.codeInsight.imports.AddImportHelper; +import com.jetbrains.python.psi.LanguageLevel; +import com.jetbrains.python.psi.PyElementGenerator; +import com.jetbrains.python.psi.PyExpression; +import com.jetbrains.python.psi.PyExpressionStatement; +import org.jetbrains.annotations.NotNull; + +public class PyUnpackTypeVarTupleQuickFix implements LocalQuickFix { + @Override + @NotNull + public String getFamilyName() { + return PyPsiBundle.message("QFIX.NAME.unpack.type.var.tuple"); + } + + public static void replaceToTypingExtensionsUnpack(@NotNull PsiElement elementTpReplace, @NotNull PsiElement elementInUnpack, + @NotNull PsiFile file, @NotNull Project project) { + AddImportHelper.addOrUpdateFromImportStatement(file, "typing_extensions", "Unpack", null, + AddImportHelper.ImportPriority.FUTURE, null); + + String unpacked = "Unpack[" + elementInUnpack.getText() + "]"; + PyElementGenerator elementGenerator = PyElementGenerator.getInstance(project); + PyExpressionStatement expressionStatement = + elementGenerator.createFromText(LanguageLevel.forElement(elementInUnpack), PyExpressionStatement.class, unpacked); + PyExpression newElement = expressionStatement.getExpression(); + + elementTpReplace.replace(newElement); + } + + @Override + public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { + PsiElement psiElement = descriptor.getPsiElement(); + if (psiElement == null) return; + + var languageLevel = LanguageLevel.forElement(psiElement); + PyElementGenerator elementGenerator = PyElementGenerator.getInstance(project); + + if (languageLevel.isAtLeast(LanguageLevel.PYTHON311)) { + String starred = "*" + psiElement.getText(); + PyExpressionStatement expressionStatement = elementGenerator.createFromText(languageLevel, PyExpressionStatement.class, starred); + PyExpression newElement = expressionStatement.getExpression(); + psiElement.replace(newElement); + } + else { + replaceToTypingExtensionsUnpack(psiElement, psiElement, psiElement.getContainingFile(), project); + } + } +} diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyCallExpressionHelper.java b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyCallExpressionHelper.java index 462b8fa5ac89..340ea63d4f6f 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyCallExpressionHelper.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyCallExpressionHelper.java @@ -813,6 +813,7 @@ public final class PyCallExpressionHelper { implicitParameters, mappingResults.getMappedParameters(), mappingResults.getUnmappedParameters(), + mappingResults.getUnmappedContainerParameters(), mappingResults.getUnmappedArguments(), mappingResults.getParametersMappedToVariadicPositionalArguments(), mappingResults.getParametersMappedToVariadicKeywordArguments(), @@ -880,6 +881,7 @@ public final class PyCallExpressionHelper { implicitParameters, mappingResults.getMappedParameters(), mappingResults.getUnmappedParameters(), + mappingResults.getUnmappedContainerParameters(), mappingResults.getUnmappedArguments(), mappingResults.getParametersMappedToVariadicPositionalArguments(), mappingResults.getParametersMappedToVariadicKeywordArguments(), @@ -1011,6 +1013,7 @@ public final class PyCallExpressionHelper { boolean mappedVariadicArgumentsToParameters = false; final Map mappedParameters = new LinkedHashMap<>(); final List unmappedParameters = new ArrayList<>(); + final List unmappedContainerParameters = new ArrayList<>(); final List unmappedArguments = new ArrayList<>(); final List parametersMappedToVariadicKeywordArguments = new ArrayList<>(); final List parametersMappedToVariadicPositionalArguments = new ArrayList<>(); @@ -1039,6 +1042,9 @@ public final class PyCallExpressionHelper { if (variadicPositionalArguments.size() == 1) { mappedParameters.put(variadicPositionalArguments.get(0), parameter); } + if (variadicPositionalArguments.size() != 1 && allPositionalArguments.size() == 0) { + unmappedContainerParameters.add(parameter); + } allPositionalArguments.clear(); variadicPositionalArguments.clear(); } @@ -1158,7 +1164,7 @@ public final class PyCallExpressionHelper { unmappedArguments.addAll(variadicPositionalArguments); unmappedArguments.addAll(variadicKeywordArguments); - return new ArgumentMappingResults(mappedParameters, unmappedParameters, unmappedArguments, + return new ArgumentMappingResults(mappedParameters, unmappedParameters, unmappedContainerParameters, unmappedArguments, parametersMappedToVariadicPositionalArguments, parametersMappedToVariadicKeywordArguments, tupleMappedParameters); } @@ -1259,6 +1265,7 @@ public final class PyCallExpressionHelper { public static class ArgumentMappingResults { @NotNull private final Map myMappedParameters; @NotNull private final List myUnmappedParameters; + @NotNull private final List myUnmappedContainerParameters; @NotNull private final List myUnmappedArguments; @NotNull private final List myParametersMappedToVariadicPositionalArguments; @NotNull private final List myParametersMappedToVariadicKeywordArguments; @@ -1266,12 +1273,14 @@ public final class PyCallExpressionHelper { ArgumentMappingResults(@NotNull Map mappedParameters, @NotNull List unmappedParameters, + @NotNull List unmappedContainerParameters, @NotNull List unmappedArguments, @NotNull List parametersMappedToVariadicPositionalArguments, @NotNull List parametersMappedToVariadicKeywordArguments, @NotNull Map mappedTupleParameters) { myMappedParameters = mappedParameters; myUnmappedParameters = unmappedParameters; + myUnmappedContainerParameters = unmappedContainerParameters; myUnmappedArguments = unmappedArguments; myParametersMappedToVariadicPositionalArguments = parametersMappedToVariadicPositionalArguments; myParametersMappedToVariadicKeywordArguments = parametersMappedToVariadicKeywordArguments; @@ -1307,6 +1316,11 @@ public final class PyCallExpressionHelper { public Map getMappedTupleParameters() { return myMappedTupleParameters; } + + @NotNull + public List getUnmappedContainerParameters() { + return myUnmappedContainerParameters; + } } private static class TupleMappingResults { diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/stubs/PyTypingAliasStubType.java b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/stubs/PyTypingAliasStubType.java index b224a5f6360a..f31b0a4a7e4b 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/stubs/PyTypingAliasStubType.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/stubs/PyTypingAliasStubType.java @@ -114,7 +114,10 @@ public class PyTypingAliasStubType extends CustomTargetExpressionStubType myElementTypes; + + public PyGenericVariadicType(@NotNull String name) { + this(name, false, null, null); + } + + public PyGenericVariadicType(@NotNull String name, boolean isHomogeneous, @Nullable List elementTypes, + @Nullable PyQualifiedNameOwner scopeOwner) { + super(name, null, false, null); + myElementTypes = elementTypes; + myIsHomogeneous = isHomogeneous; + } + + public PyGenericVariadicType(@NotNull String name, boolean isDefinition, @Nullable PyTargetExpression target, + boolean isHomogeneous, @Nullable List elementTypes, @Nullable PyQualifiedNameOwner scopeOwner) { + super(name, null, isDefinition, target); + myElementTypes = elementTypes; + myIsHomogeneous = isHomogeneous; + if (scopeOwner != null) { + setScopeOwner(scopeOwner); + } + } + + @NotNull + @Override + public PyGenericType withScopeOwner(@Nullable PyQualifiedNameOwner scopeOwner) { + return new PyGenericVariadicType(myName, isDefinition(), getDeclarationElement(), myIsHomogeneous, myElementTypes, scopeOwner); + } + + @NotNull + @Override + public PyGenericType withTargetExpression(@Nullable PyTargetExpression targetExpression) { + return new PyGenericVariadicType(myName, isDefinition(), targetExpression, myIsHomogeneous, myElementTypes, getScopeOwner()); + } + + @NotNull + @Override + public PyGenericVariadicType withAlias(@Nullable PyTargetExpression alias) { + return new PyGenericVariadicType(myName, isDefinition(), alias, myIsHomogeneous, myElementTypes, getScopeOwner()); + } + + @NotNull + public PyGenericVariadicType withDifferentName() { + return new PyGenericVariadicType(myName + "142", isDefinition(), null, myIsHomogeneous, myElementTypes, getScopeOwner()); + } + + @NotNull + public PyGenericVariadicType withElementTypes(boolean isHomogeneous, @NotNull List elementTypes) { + var resultElementTypes = new ArrayList<>(elementTypes); + for (int i = 0; i < elementTypes.size(); ++i) { + var elementType = elementTypes.get(i); + if (equals(elementType)) { + resultElementTypes.set(i, ((PyGenericVariadicType)elementType).withDifferentName()); + } + } + return new PyGenericVariadicType(myName, isDefinition(), getDeclarationElement(), isHomogeneous, resultElementTypes, getScopeOwner()); + } + + @NotNull + @Override + public PyGenericVariadicType toggleIsDefinition() { + return new PyGenericVariadicType(myName, !isDefinition(), getDeclarationElement(), myIsHomogeneous, myElementTypes, getScopeOwner()); + } + + @NotNull + public static PyGenericVariadicType fromElementTypes(@NotNull List elementTypes) { + return new PyGenericVariadicType("", false, elementTypes, null); + } + + @NotNull + public static PyGenericVariadicType homogeneous(@Nullable PyType type) { + var elementTypes = new ArrayList(); + elementTypes.add(type); + return new PyGenericVariadicType("", true, elementTypes, null); + } + + @NotNull + @Override + public String getName() { + if (myElementTypes == null) { + return "*" + myName; + } + else { + return "*" + getElementTypesToStr(); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final PyGenericVariadicType type = (PyGenericVariadicType)o; + return myName.equals(type.myName) && isDefinition() == type.isDefinition() && Objects.equals(getScopeOwner(), type.getScopeOwner()) && + Objects.equals(myElementTypes, type.myElementTypes); + } + + @Override + public int hashCode() { + int res = myName.hashCode(); + if (myElementTypes == null) return res; + return res + myElementTypes.hashCode(); + } + + @NotNull + public String getElementTypesToStr() { + if (myElementTypes == null) return ""; + StringBuilder res = new StringBuilder(); + res.append("("); + for (int i = 0; i < myElementTypes.size(); ++i) { + var type = myElementTypes.get(i); + var name = type != null ? type.getName() : "Any"; + res.append(name); + if (i < myElementTypes.size() - 1) { + res.append(","); + } + } + if (isHomogeneous()) { + res.append(", ..."); + } + res.append(")"); + return res.toString(); + } + + public boolean isHomogeneous() { + return myIsHomogeneous; + } + + public boolean isMapped(@NotNull Map typeVarTuples) { + if (myIsHomogeneous) return false; + if (myElementTypes != null && typeVarTuples.containsKey(this)) { + assert false; + } + if (myElementTypes != null) return true; + return typeVarTuples.containsKey(this) && typeVarTuples.get(this).myElementTypes != null; + } + + @Nullable + public List getMappedElementTypes(@NotNull Map typeVarTuples) { + if (myIsHomogeneous) return null; + if (myElementTypes != null && typeVarTuples.containsKey(this)) { + assert false; + } + if (myElementTypes != null) return myElementTypes; + + if (!typeVarTuples.containsKey(this)) return null; + var mapped = typeVarTuples.get(this); + if (mapped == null) return null; + if (mapped.isHomogeneous()) { + return List.of(mapped); + } + if (mapped.myElementTypes == null && !mapped.toString().equals(toString())) { + return List.of(mapped); + } + return mapped.myElementTypes; + } + + @Nullable + public PyType getIteratedItemType() { + if (myElementTypes == null) return null; + return PyUnionType.union(myElementTypes); + } +} diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyNotMatchedType.java b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyNotMatchedType.java new file mode 100644 index 000000000000..4df3512bffa9 --- /dev/null +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyNotMatchedType.java @@ -0,0 +1,50 @@ +// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.jetbrains.python.psi.types; + +import com.intellij.psi.PsiElement; +import com.intellij.util.ArrayUtilRt; +import com.intellij.util.ProcessingContext; +import com.jetbrains.python.psi.AccessDirection; +import com.jetbrains.python.psi.PyExpression; +import com.jetbrains.python.psi.resolve.PyResolveContext; +import com.jetbrains.python.psi.resolve.RatedResolveResult; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + + +public class PyNotMatchedType implements PyType { + public static final PyNotMatchedType INSTANCE = new PyNotMatchedType(); + + protected PyNotMatchedType() { + } + + @Override + @Nullable + public List resolveMember(@NotNull final String name, + @Nullable PyExpression location, + @NotNull AccessDirection direction, + @NotNull PyResolveContext resolveContext) { + return null; + } + + @Override + public Object[] getCompletionVariants(String completionPrefix, PsiElement location, ProcessingContext context) { + return ArrayUtilRt.EMPTY_OBJECT_ARRAY; + } + + @Override + public String getName() { + return "no type"; + } + + @Override + public boolean isBuiltin() { + return false; + } + + @Override + public void assertValid(String message) { + } +} diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeChecker.java b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeChecker.java index 451023b92832..7b5bfb54aaf2 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeChecker.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeChecker.java @@ -1,9 +1,9 @@ // Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.jetbrains.python.psi.types; -import com.google.common.collect.Lists; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.RecursionManager; +import com.intellij.openapi.util.Trinity; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.ResolveResult; @@ -30,8 +30,7 @@ import java.util.*; import java.util.function.Function; import static com.jetbrains.python.PyNames.FUNCTION; -import static com.jetbrains.python.psi.PyUtil.as; -import static com.jetbrains.python.psi.PyUtil.getReturnTypeToAnalyzeAsCallType; +import static com.jetbrains.python.psi.PyUtil.*; import static com.jetbrains.python.psi.impl.PyCallExpressionHelper.*; public final class PyTypeChecker { @@ -42,7 +41,7 @@ public final class PyTypeChecker { * See {@link PyTypeChecker#match(PyType, PyType, TypeEvalContext, Map)} for description. */ public static boolean match(@Nullable PyType expected, @Nullable PyType actual, @NotNull TypeEvalContext context) { - return match(expected, actual, new MatchContext(context, new HashMap<>())).orElse(true); + return match(expected, actual, getMatchContext(context, new GenericSubstitutions())).orElse(true); } /** @@ -64,8 +63,9 @@ public final class PyTypeChecker { public static boolean match(@Nullable PyType expected, @Nullable PyType actual, @NotNull TypeEvalContext context, - @NotNull Map substitutions) { - return match(expected, actual, new MatchContext(context, substitutions)).orElse(true); + @NotNull Map typeVars) { + var substitutions = new GenericSubstitutions(typeVars, new HashMap<>(), new HashMap<>(), null); + return match(expected, actual, getMatchContext(context, substitutions)).orElse(true); } public static boolean match(@Nullable PyType expected, @@ -117,6 +117,10 @@ public final class PyTypeChecker { } } + if (expected instanceof PyGenericVariadicType) { + return Optional.of(match((PyGenericVariadicType)expected, actual, context)); + } + if (actual instanceof PyGenericType && context.reversedSubstitutions) { return Optional.of(match((PyGenericType)actual, expected, context)); } @@ -258,6 +262,39 @@ public final class PyTypeChecker { return true; } + private static boolean match(@NotNull PyGenericVariadicType expected, @Nullable PyType actual, @NotNull MatchContext context) { + if (actual instanceof final PyGenericVariadicType actualGenericVariadic) { + if (expected.isMapped(context.mySubstitutions.typeVarTuples) || actualGenericVariadic.isMapped(context.mySubstitutions.typeVarTuples)) { + return matchElementTypes(List.of(expected), List.of(actualGenericVariadic), context, false, true); + } + + if (expected.isHomogeneous() && actualGenericVariadic.isHomogeneous()) { + return match(expected.getIteratedItemType(), actualGenericVariadic.getIteratedItemType(), context).orElse(false); + } + + if (!actualGenericVariadic.isHomogeneous() && context.reversedSubstitutions) { + context.mySubstitutions.typeVarTuples.put(actualGenericVariadic, expected); + } + if (!expected.isHomogeneous() && !context.reversedSubstitutions) { + context.mySubstitutions.typeVarTuples.put(expected, actualGenericVariadic); + } + return true; + } + + if (actual instanceof PyUnionType) { + return false; + } + + if (expected.isHomogeneous()) { + return match(expected.getIteratedItemType(), actual, context).orElse(false); + } + + var elementTypes = new ArrayList(); + elementTypes.add(actual); + context.mySubstitutions.typeVarTuples.put(expected, expected.withElementTypes(false, elementTypes)); + return true; + } + private static boolean match(@NotNull PyParamSpecType expected, @Nullable PyType actual, @NotNull MatchContext context) { if (actual == null) return true; if (!(actual instanceof PyParamSpecType callableActual)) return false; @@ -415,20 +452,391 @@ public final class PyTypeChecker { return elementType; } + @NotNull + public static List substituteGenericVariadicInElementTypes(@NotNull List elementTypes, @NotNull MatchContext context) { + var genericVariadics = + ContainerUtil.findAll(elementTypes, it -> it instanceof PyGenericVariadicType && + ((PyGenericVariadicType)it).isMapped(context.mySubstitutions.typeVarTuples)); + if (genericVariadics.isEmpty()) { + return elementTypes; + } + + List result = new ArrayList<>(); + for (var elementType: elementTypes) { + if (elementType instanceof PyGenericVariadicType) { + var substitution = ((PyGenericVariadicType)elementType).getMappedElementTypes(context.mySubstitutions.typeVarTuples); + if (substitution != null) { + substitution = substituteGenericVariadicInElementTypes(substitution, context); + result.addAll(substitution); + } + } + else { + result.add(elementType); + } + } + + return result; + } + + @NotNull + private static List getVariadicGenerics(@NotNull List elementTypes) { + return ContainerUtil.findAll(elementTypes, it -> it instanceof PyGenericVariadicType); + } + + public static boolean matchElementTypes(@NotNull List expectedElementTypes, @NotNull List actualElementTypes, + @NotNull MatchContext context, boolean exactMatch, boolean collectEqualsTypeVars) { + return matchElementTypes(expectedElementTypes, actualElementTypes, context, exactMatch, false, collectEqualsTypeVars, + new ArrayList<>()); + } + + private static boolean matchRestExpectedTypesWithEmptyActualTypes(@NotNull List expectedElementTypes, + @NotNull MatchContext context, boolean exactMatch, boolean reversed, + @NotNull List> matchingResult, + @Nullable PyType expectedElementType, int iExp, int expectedSize) { + if (expectedElementType instanceof final PyGenericVariadicType genericVariadicExpectedType && + (!exactMatch || iExp == expectedSize - 1)) { + context.mySubstitutions.typeVarTuples + .put(genericVariadicExpectedType, genericVariadicExpectedType.withElementTypes(false, Collections.emptyList())); + return true; + } + if (expectedElementType instanceof final PyParamSpecType paramSpecType) { + if (!context.mySubstitutions.paramSpecs.containsKey(paramSpecType)) { + context.mySubstitutions.paramSpecs.put(paramSpecType, paramSpecType.withParameters(Collections.emptyList(), context.context)); + } + return true; + } + + var restTypes = new ArrayList(); + while (iExp < expectedSize) { + restTypes.add(expectedElementTypes.get(iExp)); + iExp++; + } + var restGenericVariadic = PyGenericVariadicType.fromElementTypes(restTypes); + if (reversed) { + matchingResult.add(new Trinity<>(restGenericVariadic, PyNotMatchedType.INSTANCE, false)); + } + else { + matchingResult.add(new Trinity<>(PyNotMatchedType.INSTANCE, restGenericVariadic, false)); + } + return !exactMatch; + } + + private static boolean matchRestActualTypesWithEmptyExpectedTypes(@NotNull List actualElementTypes, boolean exactMatch, + boolean reversed, + @NotNull List> matchingResult, + int iAct, int actualSize) { + boolean flagIteration = false; + while (iAct < actualSize) { + flagIteration = true; + if (!reversed) { + matchingResult.add(new Trinity<>(actualElementTypes.get(iAct), PyNotMatchedType.INSTANCE, false)); + } + else { + matchingResult.add(new Trinity<>(PyNotMatchedType.INSTANCE, actualElementTypes.get(iAct), false)); + } + iAct++; + } + return !flagIteration || !exactMatch; + } + + private static boolean matchNonGenericVariadics(@Nullable PyType expectedElementType, @Nullable PyType actualElementType, + @NotNull MatchContext context, boolean reversed, boolean collectEqualsTypeVars, + @NotNull List> matchingResult) { + var expected = reversed ? actualElementType : expectedElementType; + var actual = reversed ? expectedElementType : actualElementType; + var matched = match(expected, actual, context).orElse(true); + + if (collectEqualsTypeVars && expected instanceof PyGenericType && expected.equals(actual) && + !context.mySubstitutions.typeVars.containsKey(expected)) { + context.mySubstitutions.typeVars.put((PyGenericType)expected, actual); + } + + matchingResult.add(new Trinity<>(actual, expected, matched)); + + return matched; + } + + private static boolean matchGenericVariadicWithElementTypes(@NotNull PyGenericVariadicType genericVariadicExpectedType, + @NotNull List actualElementTypes, + @NotNull MatchContext context, boolean reversed, + @NotNull List> matchingResult, + int suffixExpSize, int suffixActSize, int iAct) { + if (suffixActSize < suffixExpSize) { + return false; + } + + List actualElementTypesSublist = new ArrayList<>(); + for (int i = iAct; i < iAct + suffixActSize - suffixExpSize; i++) { + actualElementTypesSublist.add(actualElementTypes.get(i)); + } + + return matchGenericVariadicWithElementTypes(genericVariadicExpectedType, actualElementTypesSublist, context, reversed, matchingResult); + } + + /** + * Match two lists of types with expanding generic variadics + * + * {@code reversed} flag is to consider only {@code PyGenericVariadicType} in {@code expectedElementTypes} after all substitutions + * + * {@code exactMatch} flag is either exact match types or possibly match with nulls and not check element counts + */ + public static boolean matchElementTypes(@NotNull List expectedElementTypes, @NotNull List actualElementTypes, + @NotNull MatchContext context, boolean exactMatch, boolean reversed, + boolean collectEqualsTypeVars, + @NotNull List> matchingResult) { + expectedElementTypes = substituteGenericVariadicInElementTypes(expectedElementTypes, context); + actualElementTypes = substituteGenericVariadicInElementTypes(actualElementTypes, context); + + var matchedBoth = tryMatchElementTypesBothGenericVariadic(expectedElementTypes, actualElementTypes, context, exactMatch, reversed, + collectEqualsTypeVars, matchingResult); + if (matchedBoth.isPresent()) return matchedBoth.get(); + + boolean allMatched = true; + int expectedSize = expectedElementTypes.size(); + int actualSize = actualElementTypes.size(); + int iExp = 0; + int iAct = 0; + while (iExp < expectedSize) { + PyType expectedElementType = expectedElementTypes.get(iExp); + PyType actualElementType; + if (iAct >= actualSize) { + return matchRestExpectedTypesWithEmptyActualTypes(expectedElementTypes, context, exactMatch, reversed, matchingResult, + expectedElementType, iExp, expectedSize); + } + else { + actualElementType = actualElementTypes.get(iAct); + } + + if (expectedElementType instanceof PyParamSpecType) { + context.mySubstitutions.paramSpecs.put((PyParamSpecType)expectedElementType, as(actualElementType, PyParamSpecType.class)); + iExp++; + iAct++; + continue; + } + + if (!(expectedElementType instanceof PyGenericVariadicType)) { + allMatched &= matchNonGenericVariadics(expectedElementType, actualElementType, context, reversed, collectEqualsTypeVars, + matchingResult); + iExp++; + iAct++; + continue; + } + + int suffixExpSize = expectedSize - iExp - 1; + int suffixActSize = actualSize - iAct; + allMatched &= matchGenericVariadicWithElementTypes((PyGenericVariadicType)expectedElementType, actualElementTypes, context, reversed, + matchingResult, suffixExpSize, suffixActSize, iAct); + + iExp++; + iAct += suffixActSize - suffixExpSize; + } + + return allMatched && matchRestActualTypesWithEmptyExpectedTypes(actualElementTypes, exactMatch, reversed, matchingResult, iAct, + actualSize); + } + + private static boolean matchGenericVariadicWithElementTypes(@NotNull PyGenericVariadicType genericVariadicType, + @NotNull List elementTypes, + @NotNull MatchContext context, boolean reversed, + @NotNull List> matchingResult) { + if (genericVariadicType.isHomogeneous()) { + PyType iteratedType = genericVariadicType.getIteratedItemType(); + boolean elementsMatched = true; + for (var elementType: elementTypes) { + boolean matched; + if (elementType instanceof PyGenericVariadicType) { + matched = reversed ? + match(elementType, genericVariadicType, context).orElse(false) : + match(genericVariadicType, elementType, context); + } + else { + matched = reversed ? + match(elementType, iteratedType, context).orElse(false) : + match(iteratedType, elementType, context).orElse(false); + } + elementsMatched &= matched; + if (!reversed) { + matchingResult.add(new Trinity<>(elementType, iteratedType, matched)); + } + } + if (reversed) { + matchingResult.add(new Trinity<>(genericVariadicType, + genericVariadicType.withElementTypes(false, elementTypes), elementsMatched)); + } + return elementsMatched; + } + else { + var expectedGenericVariadicMatch = genericVariadicType.withElementTypes(false, elementTypes); + context.mySubstitutions.typeVarTuples + .put(genericVariadicType, expectedGenericVariadicMatch); + if (!reversed) { + matchingResult.addAll(ContainerUtil.map(elementTypes, it -> new Trinity<>(it, genericVariadicType, true))); + } + else { + matchingResult.add(new Trinity<>(genericVariadicType, expectedGenericVariadicMatch, true)); + } + return true; + } + } + + // Expected: Ts*, T1, T2, ... Tn + // Actual: V1, V2, ... Vm, Ts1* + private static boolean matchMiddlePartElementTypesBothGenericVariadic(@NotNull List expectedElementTypes, + @NotNull List actualElementTypes, + @NotNull MatchContext context, boolean reversed, + @NotNull List> matchingResult) { + if (expectedElementTypes.size() == 1) { + return matchGenericVariadicWithElementTypes((PyGenericVariadicType)expectedElementTypes.get(0), actualElementTypes, context, + reversed, matchingResult); + } + if (actualElementTypes.size() == 1) { + return matchGenericVariadicWithElementTypes((PyGenericVariadicType)actualElementTypes.get(0), expectedElementTypes, context, + !reversed, matchingResult); + } + + var expectedGenericVariadic = (PyGenericVariadicType)expectedElementTypes.get(0); + var actualGenericVariadic = (PyGenericVariadicType)actualElementTypes.get(actualElementTypes.size() - 1); + + // In case of Ts* and Ts1* are both not homogeneous its possible to match + // Ts* -> (V1, V2, ... Vm, Any, Any ...) + // Ts1* -> (Any, Any, ..., T1, T2, ..., Tn) + if (!expectedGenericVariadic.isHomogeneous() && !actualGenericVariadic.isHomogeneous()) { + var expectedMatch = new ArrayList<>(actualElementTypes.subList(0, actualElementTypes.size() - 1)); + expectedMatch.add(PyGenericVariadicType.homogeneous(null)); + var expectedGenericVariadicMatch = actualGenericVariadic.withElementTypes(false, expectedMatch); + context.mySubstitutions.typeVarTuples.put(expectedGenericVariadic, expectedGenericVariadicMatch); + var actualMatch = new ArrayList(); + actualMatch.add(PyGenericVariadicType.homogeneous(null)); + actualMatch.addAll(expectedElementTypes.subList(1, expectedElementTypes.size())); + var actualGenericVariadicMatch = actualGenericVariadic.withElementTypes(false, actualMatch); + context.mySubstitutions.typeVarTuples.put(actualGenericVariadic, actualGenericVariadicMatch); + if (!reversed) { + matchingResult.addAll(ContainerUtil.map(expectedMatch, it -> new Trinity<>(it, expectedGenericVariadic, true))); + matchingResult.add(new Trinity<>(actualGenericVariadic, actualGenericVariadicMatch, true)); + } + else { + matchingResult.add(new Trinity<>(expectedGenericVariadic, expectedGenericVariadicMatch, true)); + matchingResult.addAll(ContainerUtil.map(actualMatch, it -> new Trinity<>(it, actualGenericVariadic, true))); + } + return true; + } + + // In case of Ts* or Ts1* is homogeneous we need to brute force prefix of actual elements and find first match + // Try match: + // Ts* -> (V1, V2, ... V[elementSz-1]) + // (T1, T2, ..., Tn) -> (V[elementTz], V[elementSz+1], ..., Vm, Ts1*) - is case with only one generic variadic generic + // Also include the cases of split Ts1* if Ts1* is homogeneous: + // Ts* -> (V1, ..., Vn, Ts1*) + // (T1, ..., Tn) -> Ts1* + int start = reversed ? 0 : actualElementTypes.size(); + int step = reversed ? 1 : -1; + int end = reversed ? -1 : actualElementTypes.size() + 1; + var curContext = context.copy(); + var curMatchingResult = new ArrayList<>(matchingResult); + var expectedSuffix = expectedElementTypes.subList(1, expectedElementTypes.size()); + for (int elementsSz = start; elementsSz != end; elementsSz += step) { + curContext = context.copy(); + curMatchingResult = new ArrayList<>(matchingResult); + + var actElements = actualElementTypes.subList(0, elementsSz); + boolean allMatched = matchGenericVariadicWithElementTypes(expectedGenericVariadic, actElements, curContext, reversed, curMatchingResult); + + // last element can be split into 2 parts so Math.min to avoid empty + var actualSuffix = actualElementTypes.subList(Math.min(elementsSz, actualElementTypes.size() - 1), actualElementTypes.size()); + allMatched &= matchElementTypes(expectedSuffix, actualSuffix, curContext, true, reversed, false, curMatchingResult); + if (!allMatched) continue; + + matchingResult.clear(); + matchingResult.addAll(curMatchingResult); + context.mySubstitutions.putAll(curContext.mySubstitutions); + return true; + } + + matchingResult.clear(); + matchingResult.addAll(curMatchingResult); + return false; + } + + private static Optional tryMatchElementTypesBothGenericVariadic(@NotNull List expectedElementTypes, + @NotNull List actualElementTypes, + @NotNull MatchContext context, boolean exactMatch, + boolean reversed, boolean collectEqualsTypeVars, + @NotNull List> matchingResult) { + expectedElementTypes = substituteGenericVariadicInElementTypes(expectedElementTypes, context); + actualElementTypes = substituteGenericVariadicInElementTypes(actualElementTypes, context); + + var expectedGenericVariadics = getVariadicGenerics(expectedElementTypes); + var actualGenericVariadics = getVariadicGenerics(actualElementTypes); + + if (!actualGenericVariadics.isEmpty()) { + if (!expectedGenericVariadics.isEmpty()) { + if (expectedElementTypes.size() == 1 && actualElementTypes.size() == 1) { + var expected = (PyGenericVariadicType)expectedElementTypes.get(0); + var actual = (PyGenericVariadicType)actualElementTypes.get(0); + return Optional.of(match(expected, actual, context)); + } + return Optional.of(matchElementTypesBothGenericVariadic(expectedElementTypes, actualElementTypes, context, reversed, matchingResult)); + } + return Optional.of(matchElementTypes(actualElementTypes, expectedElementTypes, context, exactMatch, !reversed, collectEqualsTypeVars, + matchingResult)); + } + + return Optional.empty(); + } + + // Here we want to simplify all cases to the case: + // ..., Ts*, T1, T2, ... Tn, ... + // ..., V1, V2, ..., Vn, Ts1*, ... + // Use reverse for expected and actual in the case of Ts1* goes earlier than Ts* + // Prefix size determined by the position of Ts* and the suffix by the position of Ts1* + private static boolean matchElementTypesBothGenericVariadic(@NotNull List expectedElementTypes, @NotNull List actualElementTypes, + @NotNull MatchContext context, boolean reversed, + @NotNull List> matchingResult) { + int indExpGenericVariadic = -1; + int indActGenericVariadic = -1; + for (int i = 0; i < Math.max(expectedElementTypes.size(), actualElementTypes.size()); ++i) { + if (i < expectedElementTypes.size() && expectedElementTypes.get(i) instanceof PyGenericVariadicType) { + indExpGenericVariadic = i; + } + if (i < actualElementTypes.size() && actualElementTypes.get(i) instanceof PyGenericVariadicType) { + if (indExpGenericVariadic == -1) { + return matchElementTypesBothGenericVariadic(actualElementTypes, expectedElementTypes, context, !reversed, matchingResult); + } + else { + indActGenericVariadic = i; + break; + } + } + } + assert indExpGenericVariadic >= 0 && indActGenericVariadic >= 0; + + boolean allMatched = true; + + int prefixLen = indExpGenericVariadic; + int suffixLen = Math.min(expectedElementTypes.size() - indExpGenericVariadic - 1, actualElementTypes.size() - indActGenericVariadic - 1); + if (prefixLen > 0) { + var expectedPrefix = expectedElementTypes.subList(0, prefixLen); + var actualPrefix = actualElementTypes.subList(0, prefixLen); + allMatched = matchElementTypes(expectedPrefix, actualPrefix, context, true, reversed, false, matchingResult); + } + + var expectedMiddlePart = expectedElementTypes.subList(prefixLen, expectedElementTypes.size() - suffixLen); + var actualMiddlePart = actualElementTypes.subList(prefixLen, actualElementTypes.size() - suffixLen); + allMatched &= matchMiddlePartElementTypesBothGenericVariadic(expectedMiddlePart, actualMiddlePart, context, reversed, matchingResult); + + if (suffixLen > 0) { + var expectedSuffix = expectedElementTypes.subList(expectedElementTypes.size() - suffixLen, expectedElementTypes.size()); + var actualSuffix = actualElementTypes.subList(actualElementTypes.size() - suffixLen, actualElementTypes.size()); + allMatched &= matchElementTypes(expectedSuffix, actualSuffix, context, true, reversed, false, matchingResult); + } + + return allMatched; + } @NotNull private static Optional match(@NotNull PyTupleType expected, @NotNull PyTupleType actual, @NotNull MatchContext context) { if (!expected.isHomogeneous() && !actual.isHomogeneous()) { - if (expected.getElementCount() != actual.getElementCount()) { - return Optional.of(false); - } - - for (int i = 0; i < expected.getElementCount(); i++) { - if (!match(expected.getElementType(i), actual.getElementType(i), context).orElse(true)) { - return Optional.of(false); - } - } - return Optional.of(true); + return Optional.of(matchElementTypes(expected.getElementTypes(), actual.getElementTypes(), context, true, false)); } if (expected.isHomogeneous() && !actual.isHomogeneous()) { @@ -504,6 +912,94 @@ public final class PyTypeChecker { return actualAttributes.containsAll(expected.getAttributeNames()); } + @NotNull + public static MatchContext getMatchContext(@NotNull TypeEvalContext context, @NotNull GenericSubstitutions substitutions) { + return new MatchContext(context, substitutions, false); + } + + public static boolean matchCallableParameters(@NotNull List expectedParameters, + @NotNull List actualParameters, + @NotNull MatchContext matchContext) { + final TypeEvalContext context = matchContext.context; + + if (expectedParameters.size() == 1) { + final var firstExpectedParam = expectedParameters.get(0); + final var expectedParamType = firstExpectedParam.getType(context); + if (expectedParamType instanceof final PyParamSpecType expectedParamSpecType) { + matchContext.mySubstitutions.paramSpecs.put(expectedParamSpecType, expectedParamSpecType.withParameters(actualParameters, context)); + return true; + } + else if (expectedParamType instanceof final PyConcatenateType expectedConcatenateType) { + if (actualParameters.isEmpty()) { + return true; + } + final var actualParamType = actualParameters.get(0).getType(context); + final var expectedFirstTypes = expectedConcatenateType.getFirstTypes(); + + if (actualParamType instanceof final PyConcatenateType actualConcatenateType) { + final var actualFirstType = actualConcatenateType.getFirstTypes(); + if (!match(expectedFirstTypes, actualFirstType, matchContext)) { + return false; + } + } + else { + final var actualParamRightBound = Math.min(expectedFirstTypes.size(), actualParameters.size()); + final var actualFirstParamTypes = ContainerUtil + .map(actualParameters.subList(0, actualParamRightBound), it -> it.getType(context)); + + if (!match(expectedFirstTypes, actualFirstParamTypes, matchContext)) { + return false; + } + + if (actualParamRightBound < actualParameters.size()) { + final var expectedParamSpecType = expectedConcatenateType.getParamSpec(); + final var restActualParameters = actualParameters.subList(actualParamRightBound, actualParameters.size()); + final var parametersSubst = expectedParamSpecType.withParameters(restActualParameters, context); + matchContext.mySubstitutions.paramSpecs.put(expectedParamSpecType, parametersSubst); + return true; + } + } + + return true; + } + } + + int startIndex = 0; + if (!expectedParameters.isEmpty() && !actualParameters.isEmpty()) { + var firstExpectedParam = expectedParameters.get(0); + var firstActualParam = actualParameters.get(0); + if (firstExpectedParam.isSelf() && firstActualParam.isSelf()) { + if (!match(firstExpectedParam.getType(context), firstActualParam.getType(context), matchContext).orElse(true)) { + return false; + } + startIndex = 1; + } + } + + List expectedElementTypes = new ArrayList<>(); + List actualElementTypes = new ArrayList<>(); + + final int size = Math.max(expectedParameters.size(), actualParameters.size()); + for (int i = startIndex; i < size; i++) { + final var expectedParam = i < expectedParameters.size() ? expectedParameters.get(i) : null; + final var actualParam = i < actualParameters.size() ? actualParameters.get(i) : null; + if (actualParam != null) { + boolean couldBeMapped = expectedParam == null || couldBeMappedOntoPositionalContainer(expectedParam); + final PyType actualParamType = + actualParam.isPositionalContainer() && couldBeMapped + ? actualParam.getArgumentType(context) + : actualParam.getType(context); + actualElementTypes.add(actualParamType); + } + if (expectedParam != null) { + expectedElementTypes.add(expectedParam.getType(context)); + } + } + + // actual callable type could accept more general parameter type + return matchElementTypes(actualElementTypes, expectedElementTypes, matchContext.reverseSubstitutions(), false, false); + } + @NotNull private static Optional match(@NotNull PyCallableType expected, @NotNull PyCallableType actual, @@ -525,64 +1021,8 @@ public final class PyTypeChecker { final List expectedParameters = expected.getParameters(context); final List actualParameters = actual.getParameters(context); if (expectedParameters != null && actualParameters != null) { - final int size = Math.min(expectedParameters.size(), actualParameters.size()); - for (int i = 0; i < size; i++) { - final var expectedParam = expectedParameters.get(i); - final var actualParam = actualParameters.get(i); - final var expectedParamType = expectedParam.getType(context); - // TODO: Check named and star params, not only positional ones - if (expectedParamType instanceof PyParamSpecType && expectedParameters.size() == 1) { - final var expectedParamSpecType = (PyParamSpecType)expectedParamType; - matchContext.mySubstitutions.paramSpecs.put(expectedParamSpecType, expectedParamSpecType.withParameters(actualParameters, context)); - break; - } - else if (expectedParamType instanceof PyConcatenateType expectedConcatenateType && expectedParameters.size() == 1) { - if (i != 0) break; - - final var actualParamType = actualParam.getType(context); - final var expectedFirstTypes = expectedConcatenateType.getFirstTypes(); - - if (actualParamType instanceof PyConcatenateType actualConcatenateType) { - final var actualFirstType = actualConcatenateType.getFirstTypes(); - if (!match(expectedFirstTypes, actualFirstType, matchContext)) { - return Optional.of(false); - } - } - else { - final var actualParamRightBound = Math.min(expectedFirstTypes.size(), actualParameters.size()); - final var actualFirstParamTypes = ContainerUtil - .map(actualParameters.subList(0, actualParamRightBound), it -> it.getType(context)); - - if (!match(expectedFirstTypes, actualFirstParamTypes, matchContext)) { - return Optional.of(false); - } - - if (actualParamRightBound < actualParameters.size()) { - final var expectedParamSpecType = expectedConcatenateType.getParamSpec(); - final var restActualParameters = actualParameters.subList(actualParamRightBound, actualParameters.size()); - final var parametersSubst = expectedParamSpecType.withParameters(restActualParameters, context); - matchContext.mySubstitutions.paramSpecs.put(expectedParamSpecType, parametersSubst); - } - } - - break; - } - else if (expectedParam.isSelf() && actualParam.isSelf()) { - if (!match(expectedParam.getType(context), actualParam.getType(context), matchContext).orElse(true)) { - return Optional.of(false); - } - } - else { - final PyType actualParamType = - actualParam.isPositionalContainer() && couldBeMappedOntoPositionalContainer(expectedParam) - ? actualParam.getArgumentType(context) - : actualParam.getType(context); - - // actual callable type could accept more general parameter type - if (!match(actualParamType, expectedParam.getType(context), matchContext.reverseSubstitutions()).orElse(true)) { - return Optional.of(false); - } - } + if (!matchCallableParameters(expectedParameters, actualParameters, matchContext)) { + return Optional.of(false); } } if (!match(expected.getReturnType(context), getActualReturnType(actual, context), matchContext).orElse(true)) { @@ -687,7 +1127,15 @@ public final class PyTypeChecker { for (PyTypeProvider provider : PyTypeProvider.EP_NAME.getExtensionList()) { Map substitutionsFromClassDefinition = provider.getGenericSubstitutions(classType.getPyClass(), context); for (Map.Entry entry : substitutionsFromClassDefinition.entrySet()) { - if (entry.getKey() instanceof PyGenericType) { + if (entry.getKey() instanceof PyGenericVariadicType) { + if (entry.getValue() instanceof final PyGenericVariadicType gvt) { + result.typeVarTuples.put((PyGenericVariadicType)entry.getKey(), gvt); + } + else { + result.typeVarTuples.put((PyGenericVariadicType)entry.getKey(), null); + } + } + else if (entry.getKey() instanceof PyGenericType) { result.typeVars.put((PyGenericType)entry.getKey(), entry.getValue()); } } @@ -697,19 +1145,26 @@ public final class PyTypeChecker { List definitionTypeParameters = genericDefinitionType.getElementTypes(); List instanceTypeArguments = classType instanceof PyCollectionType ? ((PyCollectionType)classType).getElementTypes() : List.of(); - for (int i = 0; i < definitionTypeParameters.size(); i++) { - PyType typeParameter = definitionTypeParameters.get(i); - PyType typeArgument = ContainerUtil.getOrElse(instanceTypeArguments, i, null); - if (typeParameter instanceof PyGenericType) { - result.typeVars.put((PyGenericType)typeParameter, typeArgument); - } - if (typeParameter instanceof PyParamSpecType) { - result.getParamSpecs().put((PyParamSpecType)typeParameter, as(typeArgument, PyParamSpecType.class)); + + if (ContainerUtil.exists(definitionTypeParameters, it -> it instanceof PyGenericVariadicType)) { + var matchContext = getMatchContext(context, result); + matchElementTypes(definitionTypeParameters, instanceTypeArguments, matchContext, true, true); + } + else { + for (int i = 0; i < definitionTypeParameters.size(); i++) { + PyType typeParameter = definitionTypeParameters.get(i); + PyType typeArgument = ContainerUtil.getOrElse(instanceTypeArguments, i, null); + if (typeParameter instanceof PyGenericType) { + result.typeVars.put((PyGenericType)typeParameter, typeArgument); + } + else if (typeParameter instanceof PyParamSpecType) { + result.getParamSpecs().put((PyParamSpecType)typeParameter, as(typeArgument, PyParamSpecType.class)); + } } } } } - if (!result.typeVars.isEmpty()) { + if (!result.typeVars.isEmpty() || !result.typeVarTuples.isEmpty() || !result.paramSpecs.isEmpty()) { return result; } } @@ -738,11 +1193,42 @@ public final class PyTypeChecker { } List expectedElementTypes = expected.getElementTypes(); List actualElementTypes = actual.getElementTypes(); - for (int i = 0; i < expectedElementTypes.size(); i++) { - PyType subElementType = ContainerUtil.getOrElse(actualElementTypes, i, null); - if (!match(expectedElementTypes.get(i), subElementType, context).orElse(true)) { - return false; + if (actualElementTypes.isEmpty()) { + for (PyType type : expectedElementTypes) { + if (type instanceof PyGenericVariadicType) continue; + if (!match(type, (@Nullable PyType)null, context).orElse(true)) { + return false; + } } + return true; + } + + return matchElementTypes(expectedElementTypes, actualElementTypes, context, checkExactMatch(expected, actual), false); + } + + private static boolean isCollectionOfLiterals(@Nullable PyType type) { + if (type == null) return true; + if (type instanceof PyLiteralType) return true; + if (type instanceof final PyUnionType unionType) { + return ContainerUtil.all(unionType.getMembers(), it -> isCollectionOfLiterals(it)); + } + if (type instanceof final PyCollectionType collectionType) { + return ContainerUtil.all(collectionType.getElementTypes(), it -> isCollectionOfLiterals(it)); + } + return false; + } + + private static boolean checkExactMatch(@NotNull PyCollectionType expected, @NotNull PyType actual) { + if (actual instanceof final PyClassLikeType actualClassLikeType) { + String expectedQName = expected.getClassQName(); + String actualQName = actualClassLikeType.getClassQName(); + if ("typing.ValuesView".equals(expectedQName) && "_dict_values".equals(actualQName)) return false; + if ("typing.Awaitable".equals(expectedQName) && "typing.Coroutine".equals(actualQName)) return false; + if (("typing.Iterable".equals(expectedQName) || "typing.Iterator".equals(expectedQName) || + "typing.Container".equals(expectedQName)) && + "typing.Generator".equals(actualQName)) return false; + if ("typing.AsyncIterator".equals(expectedQName) && "typing.AsyncGenerator".equals(actualQName)) return false; + if (isCollectionOfLiterals(expected) && isCollectionOfLiterals(actual)) return false; } return true; } @@ -852,14 +1338,19 @@ public final class PyTypeChecker { @NotNull Generics generics, @NotNull Set visited) { if (type instanceof PyGenericType) { - generics.allTypeVars.add((PyGenericType)type); + generics.allTypeVarsAndTypeVarTuples.add((PyGenericType)type); } if (visited.contains(type)) { return; } visited.add(type); if (type instanceof PyGenericType) { - generics.typeVars.add((PyGenericType)type); + if (type instanceof PyGenericVariadicType) { + generics.typeVarTuples.add((PyGenericVariadicType)type); + } + else { + generics.typeVars.add((PyGenericType)type); + } } if (type instanceof PyParamSpecType) { generics.paramSpecs.add((PyParamSpecType)type); @@ -899,15 +1390,32 @@ public final class PyTypeChecker { } } - /** - * @deprecated use {@link PyTypeChecker#substitute(PyType, GenericSubstitutions, TypeEvalContext)} instead - */ - @Deprecated - @Nullable - public static PyType substitute(@Nullable PyType type, @NotNull Map substitutions, - @NotNull TypeEvalContext context) { - final var genericSubstitutions = new GenericSubstitutions(substitutions, new LinkedHashMap<>(), null); - return substitute(type, genericSubstitutions, context); + @NotNull + public static List<@Nullable PyType> substituteExpand(@Nullable PyType type, + @NotNull GenericSubstitutions substitutions, + @NotNull TypeEvalContext context, + @NotNull Set substituting) { + var result = new ArrayList(); + if (type instanceof final PyGenericVariadicType genericVariadicType) { + var elementTypes = genericVariadicType.getMappedElementTypes(substitutions.typeVarTuples); + if (elementTypes == null) { + if (genericVariadicType.isHomogeneous()) { + PyType homoType = genericVariadicType.getIteratedItemType(); + PyType substHomoType = substitute(homoType, substitutions, context, substituting); + if (homoType == substHomoType) { + return List.of(type); + } + else { + return List.of(PyGenericVariadicType.homogeneous(substHomoType)); + } + } + return List.of(type); + } + + return ContainerUtil.flatMap(elementTypes, it -> substituteExpand(it, substitutions, context, substituting)); + } + result.add(substitute(type, substitutions, context, substituting)); + return result; } @Nullable @@ -916,17 +1424,24 @@ public final class PyTypeChecker { } @Nullable - private static PyType substitute(@Nullable PyType type, - @NotNull GenericSubstitutions substitutions, - @NotNull TypeEvalContext context, - @NotNull Set substituting) { + public static PyType substitute(@Nullable PyType type, + @NotNull GenericSubstitutions substitutions, + @NotNull TypeEvalContext context, + @NotNull Set substituting) { boolean alreadySubstituting = !substituting.add(type); if (alreadySubstituting) { return null; } try { if (hasGenerics(type, context)) { - if (type instanceof PyGenericType typeVar) { + if (type instanceof final PyGenericVariadicType genericVariadicType) { + var expandedElementTypes = substituteExpand(type, substitutions, context, substituting); + if (expandedElementTypes.size() == 1) { + return expandedElementTypes.get(0); + } + return genericVariadicType.withElementTypes(false, expandedElementTypes); + } + if (type instanceof final PyGenericType typeVar) { PyType substitution = substitutions.typeVars.get(typeVar); if (substitution == null) { final PyInstantiableType invertedTypeVar = invert(typeVar); @@ -968,22 +1483,6 @@ public final class PyTypeChecker { else if (type instanceof PyUnionType) { return ((PyUnionType)type).map(member -> substitute(member, substitutions, context, substituting)); } - else if (type instanceof PyCollectionTypeImpl collection) { - final List elementTypes = collection.getElementTypes(); - final List substitutes = new ArrayList<>(); - for (PyType elementType : elementTypes) { - if (elementType instanceof PyParamSpecType paramSpecType) { - final var paramSpecTypeSubst = substitutions.paramSpecs.get(paramSpecType); - if (paramSpecTypeSubst != null && paramSpecTypeSubst.getParameters() != null) { - substitutes.add(paramSpecTypeSubst); - } - } - else { - substitutes.add(substitute(elementType, substitutions, context, substituting)); - } - } - return new PyCollectionTypeImpl(collection.getPyClass(), collection.isDefinition(), substitutes); - } else if (type instanceof PyTypedDictType typedDictType) { final Map> tdFields = typedDictType.getKeysToValuesWithTypes(); final var substitutedTDFields = ContainerUtil.map2Map( @@ -998,15 +1497,34 @@ public final class PyTypeChecker { ); return PyTypedDictType.Companion.createFromKeysToValueTypes(typedDictType.myClass, substitutedTDFields, false); } - else if (type instanceof PyTupleType tupleType) { + else if (type instanceof final PyCollectionTypeImpl collection) { + final List elementTypes = collection.getElementTypes(); + final List substitutes = new ArrayList<>(); + for (PyType elementType : elementTypes) { + if (elementType instanceof final PyParamSpecType paramSpecType) { + final var paramSpecTypeSubst = substitutions.paramSpecs.get(paramSpecType); + if (paramSpecTypeSubst != null && paramSpecTypeSubst.getParameters() != null) { + substitutes.add(paramSpecTypeSubst); + } + } + else { + substitutes.addAll(substituteExpand(elementType, substitutions, context, substituting)); + } + } + return new PyCollectionTypeImpl(collection.getPyClass(), collection.isDefinition(), substitutes); + } + else if (type instanceof PyTupleType) { + final PyTupleType tupleType = (PyTupleType)type; final PyClass tupleClass = tupleType.getPyClass(); final List oldElementTypes = tupleType.isHomogeneous() ? Collections.singletonList(tupleType.getIteratedItemType()) : tupleType.getElementTypes(); + // newElementTypes need to be modifiable list final List newElementTypes = - ContainerUtil.map(oldElementTypes, elementType -> substitute(elementType, substitutions, context, substituting)); + new ArrayList<>(ContainerUtil.flatMap(oldElementTypes, elementType -> + substituteExpand(elementType, substitutions, context, substituting))); return new PyTupleType(tupleClass, newElementTypes, tupleType.isHomogeneous()); } @@ -1035,12 +1553,15 @@ public final class PyTypeChecker { break; } } - final PyType substType = substitute(parameter.getType(context), substitutions, context, substituting); + + final List substTypes = substituteExpand(parameter.getType(context), substitutions, context, substituting); final PyParameter psi = parameter.getParameter(); - final PyCallableParameter subst = psi != null ? - PyCallableParameterImpl.psi(psi, substType) : - PyCallableParameterImpl.nonPsi(parameter.getName(), substType, parameter.getDefaultValue()); - substParams.add(subst); + final List substs = + psi != null ? + ContainerUtil.map(substTypes, it -> PyCallableParameterImpl.psi(psi, it)) : + ContainerUtil.map(substTypes, it -> PyCallableParameterImpl.nonPsi(parameter.getName(), it, parameter.getDefaultValue())); + + substParams.addAll(substs); } } final PyType substResult = substitute(callable.getReturnType(context), substitutions, context, substituting); @@ -1058,10 +1579,7 @@ public final class PyTypeChecker { public static GenericSubstitutions unifyGenericCall(@Nullable PyExpression receiver, @NotNull Map arguments, @NotNull TypeEvalContext context) { - final var substitutions = unifyReceiverWithParamSpecs(receiver, context); - if (arguments.isEmpty()) { - return substitutions; - } + final var substitutions = unifyReceiver(receiver, context); for (Map.Entry entry : getRegularMappedParameters(arguments).entrySet()) { final PyCallableParameter paramWrapper = entry.getValue(); final PyType expectedType = paramWrapper.getArgumentType(context); @@ -1101,27 +1619,39 @@ public final class PyTypeChecker { } } if (!matchContainer(getMappedPositionalContainer(arguments), getArgumentsMappedToPositionalContainer(arguments), - substitutions.typeVars, context)) { + substitutions, context)) { return null; } if (!matchContainer(getMappedKeywordContainer(arguments), getArgumentsMappedToKeywordContainer(arguments), - substitutions.typeVars, context)) { + substitutions, context)) { return null; } return substitutions; } private static boolean matchContainer(@Nullable PyCallableParameter container, @NotNull List arguments, - @NotNull Map substitutions, @NotNull TypeEvalContext context) { + @NotNull GenericSubstitutions substitutions, @NotNull TypeEvalContext context) { if (container == null) { return true; } - final List types = ContainerUtil.map(arguments, context::getType); - return match(container.getArgumentType(context), PyUnionType.union(types), context, substitutions); + final List actualArgumentTypes = ContainerUtil.map(arguments, context::getType); + final PyType expectedArgumentType = container.getArgumentType(context); + if (expectedArgumentType instanceof final PyGenericVariadicType genericVariadicType) { + var elementTypes = genericVariadicType.getMappedElementTypes(substitutions.typeVarTuples); + if (elementTypes != null) { + return matchElementTypes(elementTypes, actualArgumentTypes, getMatchContext(context, substitutions), true, false); + } + else { + substitutions.typeVarTuples.put(genericVariadicType, + ((PyGenericVariadicType)expectedArgumentType).withElementTypes(false, actualArgumentTypes)); + return true; + } + } + return match(expectedArgumentType, PyUnionType.union(actualArgumentTypes), context, substitutions); } @NotNull - public static GenericSubstitutions unifyReceiverWithParamSpecs(@Nullable PyExpression receiver, @NotNull TypeEvalContext context) { + public static GenericSubstitutions unifyReceiver(@Nullable PyExpression receiver, @NotNull TypeEvalContext context) { // Collect generic params of object type final var substitutions = new GenericSubstitutions(); if (receiver != null) { @@ -1140,6 +1670,9 @@ public final class PyTypeChecker { for (Map.Entry typeVarMapping : newSubstitutions.typeVars.entrySet()) { substitutions.typeVars.putIfAbsent(typeVarMapping.getKey(), typeVarMapping.getValue()); } + for (Map.Entry typeVarMapping : newSubstitutions.typeVarTuples.entrySet()) { + substitutions.typeVarTuples.putIfAbsent(typeVarMapping.getKey(), typeVarMapping.getValue()); + } for (Map.Entry paramSpecMapping : newSubstitutions.paramSpecs.entrySet()) { substitutions.paramSpecs.putIfAbsent(paramSpecMapping.getKey(), paramSpecMapping.getValue()); } @@ -1148,6 +1681,15 @@ public final class PyTypeChecker { return substitutions; } + private static void replaceUnresolvedGenericsWithAny(@NotNull Map substitutions) { + final List unresolvedGenerics = + ContainerUtil.filter(substitutions.values(), type -> type instanceof PyGenericType && !substitutions.containsKey(type)); + + for (PyType unresolvedGeneric : unresolvedGenerics) { + substitutions.put((PyGenericType)unresolvedGeneric, null); + } + } + private static boolean matchClasses(@Nullable PyClass superClass, @Nullable PyClass subClass, @NotNull TypeEvalContext context) { if (superClass == null || subClass == null || @@ -1318,12 +1860,13 @@ public final class PyTypeChecker { @NotNull TypeEvalContext context) { Generics typeParams = collectGenerics(genericType, context); if (!typeParams.isEmpty()) { - final List formalTypeParams = Lists.newArrayList(typeParams.typeVars); - final Map substitutions = new HashMap<>(); - for (int i = 0; i < Math.min(formalTypeParams.size(), actualTypeParams.size()); i++) { - substitutions.put(formalTypeParams.get(i), actualTypeParams.get(i)); - } - return substitute(genericType, new GenericSubstitutions(substitutions, Collections.emptyMap(), null), context); + var substitutions = new GenericSubstitutions(); + var matchContext = getMatchContext(context, substitutions); + List generics = new ArrayList<>(); + generics.addAll(typeParams.typeVars); + generics.addAll(typeParams.typeVarTuples); + matchElementTypes(generics, actualTypeParams, matchContext, true, true); + return substitute(genericType, substitutions, context); } // An already parameterized type, don't override existing values for type parameters else if (genericType instanceof PyCollectionType) { @@ -1342,21 +1885,57 @@ public final class PyTypeChecker { public static class Generics { @NotNull private final Set typeVars = new LinkedHashSet<>(); + @NotNull - private final List allTypeVars = new ArrayList<>(); + private final Set typeVarTuples = new LinkedHashSet<>(); + + @NotNull + private final List allTypeVarsAndTypeVarTuples = new ArrayList<>(); + @NotNull private final Set paramSpecs = new LinkedHashSet<>(); + @NotNull private final Set concatenates = new LinkedHashSet<>(); + @Nullable private PySelfType self; + //Generics() { + // this(new LinkedHashSet<>(), new LinkedHashSet<>(), new ArrayList<>(), new LinkedHashSet<>(), new LinkedHashSet<>(), null); + //} + // + //Generics(@NotNull Set generics, + // @NotNull Set genericVariadics, + // @NotNull List typeVarsAndTuples, + // @NotNull Set paramSpecs, + // @NotNull Set concatenates, + // @Nullable PySelfType self) { + // this.typeVars = generics; + // this.typeVarTuples = genericVariadics; + // this.typeVarsAndTuples = typeVarsAndTuples; + // this.paramSpecs = paramSpecs; + // this.concatenates = concatenates; + // this.self = self; + //} + public @NotNull Set getTypeVars() { return Collections.unmodifiableSet(typeVars); } - public @NotNull List getAllTypeVars() { - return Collections.unmodifiableList(allTypeVars); + public @NotNull Set getTypeVarTuples() { + return Collections.unmodifiableSet(typeVarTuples); + } + + public @NotNull List getAllTypeVarsAndTypeVarTuples() { + return Collections.unmodifiableList(allTypeVarsAndTypeVarTuples); + } + + public @NotNull List getTypeVarsAndTuples() { + var result = new ArrayList(); + result.addAll(typeVars); + result.addAll(typeVarTuples); + return result; } public @NotNull Set getParamSpecs() { @@ -1364,7 +1943,7 @@ public final class PyTypeChecker { } public boolean isEmpty() { - return typeVars.isEmpty() && paramSpecs.isEmpty() && concatenates.isEmpty() && self == null; + return typeVars.isEmpty() && typeVarTuples.isEmpty() && paramSpecs.isEmpty() && concatenates.isEmpty() && self == null; } } @@ -1373,24 +1952,29 @@ public final class PyTypeChecker { @NotNull private final Map typeVars; + @NotNull + private final Map typeVarTuples; + @NotNull private final Map paramSpecs; @Nullable private PyType qualifierType; - GenericSubstitutions() { - this(new LinkedHashMap<>(), new LinkedHashMap<>(), null); + public GenericSubstitutions(@NotNull Map typeVars) { + this(typeVars, new LinkedHashMap<>(), new LinkedHashMap<>(), null); } - public GenericSubstitutions(@NotNull Map typeVars) { - this(typeVars, new LinkedHashMap<>(), null); + public GenericSubstitutions() { + this(new LinkedHashMap<>(), new LinkedHashMap<>(), new LinkedHashMap<>(), null); } GenericSubstitutions(@NotNull Map typeVars, + @NotNull Map typeVarTuples, @NotNull Map paramSpecs, @Nullable PyType qualifierType) { this.typeVars = typeVars; + this.typeVarTuples = typeVarTuples; this.paramSpecs = paramSpecs; this.qualifierType = qualifierType; } @@ -1400,15 +1984,37 @@ public final class PyTypeChecker { return paramSpecs; } + public Map getTypeVars() { + return typeVars; + } + + public Map getTypeVarTuples() { + return typeVarTuples; + } + @Nullable public PyType getQualifierType() { return qualifierType; } + @NotNull + public GenericSubstitutions copy() { + return new GenericSubstitutions(new LinkedHashMap<>(typeVars), new LinkedHashMap<>(typeVarTuples), new LinkedHashMap<>(paramSpecs), + qualifierType); + } + + public void putAll(@NotNull GenericSubstitutions substitutions) { + typeVars.putAll(substitutions.typeVars); + typeVarTuples.putAll(substitutions.typeVarTuples); + paramSpecs.putAll(substitutions.paramSpecs); + qualifierType = substitutions.qualifierType; + } + @Override public String toString() { return "GenericSubstitutions{" + "typeVars=" + typeVars + + ", typeVarTuples" + typeVarTuples + ", paramSpecs=" + paramSpecs + '}'; } @@ -1424,11 +2030,6 @@ public final class PyTypeChecker { private final boolean reversedSubstitutions; - MatchContext(@NotNull TypeEvalContext context, - @NotNull Map typeVars) { - this(context, new GenericSubstitutions(typeVars), false); - } - MatchContext(@NotNull TypeEvalContext context, @NotNull GenericSubstitutions substitutions, boolean reversedSubstitutions) { this.context = context; this.mySubstitutions = substitutions; @@ -1439,5 +2040,9 @@ public final class PyTypeChecker { public MatchContext reverseSubstitutions() { return new MatchContext(context, mySubstitutions, !reversedSubstitutions); } + + @NotNull MatchContext copy() { + return new MatchContext(context, mySubstitutions.copy(), reversedSubstitutions); + } } } diff --git a/python/python-psi-impl/src/com/jetbrains/python/validation/CompatibilityVisitor.java b/python/python-psi-impl/src/com/jetbrains/python/validation/CompatibilityVisitor.java index bc4383c30144..15fa645a701e 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/validation/CompatibilityVisitor.java +++ b/python/python-psi-impl/src/com/jetbrains/python/validation/CompatibilityVisitor.java @@ -161,7 +161,8 @@ public abstract class CompatibilityVisitor extends PyAnnotator { level.isOlderThan(LanguageLevel.PYTHON311) && registerForLanguageLevel(level), PyPsiBundle.message("INSP.compatibility.feature.support.starred.expressions.in.subscriptions"), - node); + node, + new PyReplaceStarByUnpackQuickFix()); } } if (node.getParent() instanceof PySubscriptionExpression || node.getParent() instanceof PySliceItem) { @@ -169,7 +170,8 @@ public abstract class CompatibilityVisitor extends PyAnnotator { level.isOlderThan(LanguageLevel.PYTHON311) && registerForLanguageLevel(level), PyPsiBundle.message("INSP.compatibility.feature.support.starred.expressions.in.subscriptions"), - node); + node, + new PyReplaceStarByUnpackQuickFix()); } } diff --git a/python/testData/quickFixes/PyReplaceStarByUnpackQuickFixTest/typeVarTupleAfterTypeVar.py b/python/testData/quickFixes/PyReplaceStarByUnpackQuickFixTest/typeVarTupleAfterTypeVar.py new file mode 100644 index 000000000000..d72f7b65d20a --- /dev/null +++ b/python/testData/quickFixes/PyReplaceStarByUnpackQuickFixTest/typeVarTupleAfterTypeVar.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +from typing import Generic, TypeVar, TypeVarTuple + +DType = TypeVar('DType') +Shape = TypeVarTuple('Shape') + + +class Array(Generic[DType, *Shape]): + pass diff --git a/python/testData/quickFixes/PyReplaceStarByUnpackQuickFixTest/typeVarTupleAfterTypeVar_after.py b/python/testData/quickFixes/PyReplaceStarByUnpackQuickFixTest/typeVarTupleAfterTypeVar_after.py new file mode 100644 index 000000000000..e3a84b0b569d --- /dev/null +++ b/python/testData/quickFixes/PyReplaceStarByUnpackQuickFixTest/typeVarTupleAfterTypeVar_after.py @@ -0,0 +1,11 @@ +from __future__ import annotations +from typing_extensions import Unpack + +from typing import Generic, TypeVar, TypeVarTuple + +DType = TypeVar('DType') +Shape = TypeVarTuple('Shape') + + +class Array(Generic[DType, Unpack[Shape]]): + pass diff --git a/python/testData/quickFixes/PyReplaceStarByUnpackQuickFixTest/typeVarTupleOnly.py b/python/testData/quickFixes/PyReplaceStarByUnpackQuickFixTest/typeVarTupleOnly.py new file mode 100644 index 000000000000..bdf4b775ced4 --- /dev/null +++ b/python/testData/quickFixes/PyReplaceStarByUnpackQuickFixTest/typeVarTupleOnly.py @@ -0,0 +1,8 @@ +from typing import TypeVarTuple +from typing import Generic + +Shape = TypeVarTuple("Shape") + + +class Array(Generic[*Shape]): + ... \ No newline at end of file diff --git a/python/testData/quickFixes/PyReplaceStarByUnpackQuickFixTest/typeVarTupleOnly_after.py b/python/testData/quickFixes/PyReplaceStarByUnpackQuickFixTest/typeVarTupleOnly_after.py new file mode 100644 index 000000000000..dac66277515c --- /dev/null +++ b/python/testData/quickFixes/PyReplaceStarByUnpackQuickFixTest/typeVarTupleOnly_after.py @@ -0,0 +1,10 @@ +from typing_extensions import Unpack + +from typing import TypeVarTuple +from typing import Generic + +Shape = TypeVarTuple("Shape") + + +class Array(Generic[Unpack[Shape]]): + ... \ No newline at end of file diff --git a/python/testData/quickFixes/PyUnpackTypeVarTupleQuickFixTest/unpackWithStarExpression.py b/python/testData/quickFixes/PyUnpackTypeVarTupleQuickFixTest/unpackWithStarExpression.py new file mode 100644 index 000000000000..5edb4a1f49de --- /dev/null +++ b/python/testData/quickFixes/PyUnpackTypeVarTupleQuickFixTest/unpackWithStarExpression.py @@ -0,0 +1,10 @@ +from typing import TypeVarTuple +from typing import Generic +from typing import Tuple + +Shape = TypeVarTuple("Shape") + + +class Array(Generic[*Shape]): + def __init__(self, shape: Tuple[Shape]) -> None: + self.shape = shape \ No newline at end of file diff --git a/python/testData/quickFixes/PyUnpackTypeVarTupleQuickFixTest/unpackWithStarExpression_after.py b/python/testData/quickFixes/PyUnpackTypeVarTupleQuickFixTest/unpackWithStarExpression_after.py new file mode 100644 index 000000000000..5ca87bb710cd --- /dev/null +++ b/python/testData/quickFixes/PyUnpackTypeVarTupleQuickFixTest/unpackWithStarExpression_after.py @@ -0,0 +1,10 @@ +from typing import TypeVarTuple +from typing import Generic +from typing import Tuple + +Shape = TypeVarTuple("Shape") + + +class Array(Generic[*Shape]): + def __init__(self, shape: Tuple[*Shape]) -> None: + self.shape = shape \ No newline at end of file diff --git a/python/testData/quickFixes/PyUnpackTypeVarTupleQuickFixTest/unpackWithTypingExtensionUnpackOlderVersions.py b/python/testData/quickFixes/PyUnpackTypeVarTupleQuickFixTest/unpackWithTypingExtensionUnpackOlderVersions.py new file mode 100644 index 000000000000..b2b3fd649edb --- /dev/null +++ b/python/testData/quickFixes/PyUnpackTypeVarTupleQuickFixTest/unpackWithTypingExtensionUnpackOlderVersions.py @@ -0,0 +1,12 @@ +from typing_extensions import Unpack + +from typing import TypeVarTuple +from typing import Generic +from typing import Tuple + +Shape = TypeVarTuple("Shape") + + +class Array(Generic[Unpack[Shape]]): + def __init__(self, shape: Tuple[Shape]) -> None: + self.shape = shape \ No newline at end of file diff --git a/python/testData/quickFixes/PyUnpackTypeVarTupleQuickFixTest/unpackWithTypingExtensionUnpackOlderVersions_after.py b/python/testData/quickFixes/PyUnpackTypeVarTupleQuickFixTest/unpackWithTypingExtensionUnpackOlderVersions_after.py new file mode 100644 index 000000000000..bcb9a7faf63d --- /dev/null +++ b/python/testData/quickFixes/PyUnpackTypeVarTupleQuickFixTest/unpackWithTypingExtensionUnpackOlderVersions_after.py @@ -0,0 +1,12 @@ +from typing_extensions import Unpack + +from typing import TypeVarTuple +from typing import Generic +from typing import Tuple + +Shape = TypeVarTuple("Shape") + + +class Array(Generic[Unpack[Shape]]): + def __init__(self, shape: Tuple[Unpack[Shape]]) -> None: + self.shape = shape \ 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 c764c762c99f..4a4512cbcfc8 100644 --- a/python/testSrc/com/jetbrains/python/PyTypingTest.java +++ b/python/testSrc/com/jetbrains/python/PyTypingTest.java @@ -2114,6 +2114,904 @@ public class PyTypingTest extends PyTestCase { expr = x.method()"""); } + // PY-53105 + public void testGenericVariadicType() { + doTest("tuple[*Shape]", + """ + from typing import Generic, TypeVarTuple, Tuple + + Shape = TypeVarTuple('Shape') + + t: Tuple[*Shape] + expr = t"""); + } + + // PY-53105 + public void testGenericVariadicByCallable() { + doTest("tuple[int, str]", + """ + from typing import TypeVar, TypeVarTuple, Callable, Tuple + + Ts = TypeVarTuple('Ts') + + + def foo(f: Callable[[*Ts], Tuple[*Ts]]) -> Tuple[*Ts]: ... + def bar(a: int, b: str) -> Tuple[int, str]: ... + + + expr = foo(bar) + """); + } + + // PY-53105 + public void testGenericVariadicByCallablePrefixSuffix() { + doTest("tuple[str, str, float, int, bool]", + """ + from typing import TypeVar, TypeVarTuple, Callable, Tuple + + T = TypeVar('T') + Ts = TypeVarTuple('Ts') + + + def foo(f: Callable[[int, *Ts, T], Tuple[T, *Ts]]) -> Tuple[str, *Ts, int, T]: ... + def bar(a: int, b: str, c: float, d: bool) -> Tuple[bool, str, float]: ... + + + expr = foo(bar) + """); + } + + // PY-53105 + public void testGenericVariadicClass() { + doTest("A[float, bool, list[LiteralString]]", + """ + from typing import TypeVarTuple, Generic, Tuple + + Ts = TypeVarTuple('Ts') + + + class A(Generic[*Ts]): + def __init__(self, value: Tuple[int, *Ts]) -> None: + self.field: Tuple[int, *Ts] = value + + + tpl = (42, 1.1, True, ['42']) + expr = A(tpl) + """); + } + + // PY-53105 + public void testGenericVariadicClassField() { + doTest("tuple[int, float, bool, list[LiteralString]]", + """ + from typing import TypeVarTuple, Generic, Tuple + + Ts = TypeVarTuple('Ts') + + + class A(Generic[*Ts]): + def __init__(self, value: Tuple[int, *Ts]) -> None: + self.field: Tuple[int, *Ts] = value + + + tpl = (42, 1.1, True, ['42']) + a = A(tpl) + expr = a.field + """); + } + + // PY-53105 + public void testGenericVariadicClassMethod() { + doTest("tuple[int, bool, float, str]", + """ + from typing import TypeVarTuple, Generic, Tuple + + Ts = TypeVarTuple('Ts') + + + class A(Generic[*Ts]): + def __init__(self, value: Tuple[*Ts]) -> None: + ... + + def foo(self) -> Tuple[int, *Ts, str]: + ... + + + tpl = (True, 1.1) + a = A(tpl) + expr = a.foo() + + """); + } + + // PY-53105 + public void testGenericVariadicClassMethodPlus() { + doTest("A[int, LiteralString, bool, int]", + """ + from __future__ import annotations + from typing import TypeVarTuple, Generic, Tuple, TypeVar + + T = TypeVar('T') + Ts = TypeVarTuple('Ts') + + + class A(Generic[T, *Ts]): + def __init__(self, t: T, *args: *Ts) -> None: + ... + + def __add__(self, other: A[T, *Ts]) -> A[T, *Ts, T]: + ... + + + a = A(1, '', True) + b = A(1, '', True) + expr = a + b + """); + } + + // PY-53105 + public void testGenericVariadicAndGenericClass() { + doTest("A[int | str, int | str, list[int]]", + """ + from __future__ import annotations + from typing import TypeVarTuple, Generic, Tuple, TypeVar + + T = TypeVar('T') + T1 = TypeVar('T1') + Ts = TypeVarTuple('Ts') + + + class A(Generic[T, *Ts, T1]): + def __init__(self, t: T, tpl: Tuple[*Ts], t1: T1) -> None: + ... + + + x: int | str + expr = A(x, (x,), [1]) + """); + } + + // PY-53105 + public void testGenericVariadicClassMethodAddAxisPrefix() { + doTest("Array[LiteralString, int, bool]", + """ + from __future__ import annotations + from typing import Generic, TypeVarTuple, Tuple, NewType, TypeVar + + T = TypeVar('T') + Shape = TypeVarTuple('Shape') + + + class Array(Generic[*Shape]): + def __init__(self, shape: Tuple[*Shape]): + self._shape: Tuple[*Shape] = shape + + def add_axis_prefix(self, t: T) -> Array[T, *Shape]: ... + + + shape = (42, True) + arr: Array[int, bool] = Array(shape) + expr = arr.add_axis_prefix('') + """); + } + + // PY-53105 + public void testGenericVariadicClassMethodAddAxisSuffix() { + doTest("Array[list[int], bool, LiteralString]", + """ + from __future__ import annotations + from typing import Generic, TypeVarTuple, Tuple, NewType, TypeVar + + T = TypeVar('T') + Shape = TypeVarTuple('Shape') + + + class Array(Generic[*Shape]): + def __init__(self, shape: Tuple[*Shape]): + self._shape: Tuple[*Shape] = shape + + def add_axis_suffix(self, t: T) -> Array[*Shape, T]: ... + + + shape = ([42], True) + arr: Array[list[int], bool] = Array(shape) + expr = arr.add_axis_suffix('42') + """); + } + + // PY-53105 + public void testGenericVariadicClassMethodAddAxisPrefixAndSuffix() { + doTest("Array[LiteralString, dict[int, LiteralString], int, str, list[int], bool]", + """ + from __future__ import annotations + from typing import Generic, TypeVarTuple, Tuple, NewType, TypeVar + + T1 = TypeVar('T1') + T2 = TypeVar('T2') + T3 = TypeVar('T3') + T4 = TypeVar('T4') + Shape = TypeVarTuple('Shape') + + + class Array(Generic[*Shape]): + def __init__(self, shape: Tuple[*Shape]): + self._shape: Tuple[*Shape] = shape + + def add_axis_prefix_suffix(self, t1: T1, t2: T2, t3: T3, t4: T4) -> Array[T3, T2, *Shape, T1, T4]: ... + + + shape = (42, '42') + arr: Array[int, str] = Array(shape) + expr = arr.add_axis_prefix_suffix([42], {42: '42'}, '42', True) + """); + } + + // PY-53105 + public void testGenericVariadicFunctionAddPrefixAndSuffix() { + doTest("Array[int, list[int], bool, str]", + """ + from typing import Generic, TypeVarTuple, NewType, Tuple + + Ts = TypeVarTuple('Ts') + + + class Array(Generic[*Ts]): + def __init__(self, shape: Tuple[*Ts]): + ... + + + def add_suf_pref(x: Array[*Ts]) -> Array[int, *Ts, str]: + ... + + + ts = ([42], True) + arr = Array(ts) + expr = add_suf_pref(arr) + """); + } + + // PY-53105 + public void testGenericVariadicFunctionDeletePrefixAndSuffix() { + doTest("Array[list[int], bool]", + """ + from typing import Generic, TypeVarTuple, NewType, Tuple + + Ts = TypeVarTuple('Ts') + + + class Array(Generic[*Ts]): + def __init__(self, shape: Tuple[*Ts]): + ... + + + def del_suf_pref(x: Array[int, *Ts, str]) -> Array[*Ts]: + ... + + + ts = (42, [42], True, '42') + arr = Array(ts) + expr = del_suf_pref(arr) + """); + } + + // PY-53105 + public void testGenericVariadicStarArgs() { + doTest("tuple[int, LiteralString]", + """ + from typing import TypeVarTuple, Tuple + + Ts = TypeVarTuple('Ts') + + + def args_to_tuple(*args: *Ts) -> Tuple[*Ts]: ... + + + expr = args_to_tuple(1, 'a') + """); + } + + // PY-53105 + public void testGenericVariadicStarArgsOfGenericVariadics() { + doTest("tuple[int, LiteralString]", + """ + from typing import Tuple, TypeVarTuple + + Ts = TypeVarTuple('Ts') + + + def foo(*args: Tuple[*Ts]) -> Tuple[*Ts]: ... + + + expr = foo((0, '1'), (1, '0')) + """); + } + + // PY-53105 + public void testGenericVariadicStarArgsPrefixSuffix() { + doTest("tuple[LiteralString, list, dict, bool, int]", + """ + from typing import TypeVarTuple, Tuple + + Ts = TypeVarTuple('Ts') + + + def foo(*args: *Tuple[int, *Ts, str]) -> Tuple[*Ts, int]: ... + + + expr = foo(1, '', [], {}, True, '') + """); + } + + // PY-53105 + public void testGenericVariadicStarArgsAndTypeVars() { + doTest("tuple[LiteralString, list[int], bool, int]", + """ + from typing import TypeVarTuple, Tuple, TypeVar + + Ts = TypeVarTuple('Ts') + T1 = TypeVar('T1') + T2 = TypeVar('T2') + + + def args_to_tuple(t1: T1, t2: T2, *args: *Tuple[T2, *Ts, float]) -> Tuple[T2, *Ts, T1]: ... + + + expr = args_to_tuple(1, 'a', 'a', [1], True, 3.3) + """); + } + + // PY-53105 + public void testGenericVariadicTypeAlias() { + doTest("tuple[int, str, bool]", + """ + from typing import Tuple, TypeVarTuple + + Ts = TypeVarTuple('Ts') + + MyType = Tuple[int, *Ts] + + t: MyType[str, bool] + expr = t + """); + } + + // PY-53105 + public void testGenericVariadicAndGenericTypeAlias() { + doTest("tuple[int, str, bool, float]", + """ + from typing import Tuple, TypeVarTuple, TypeVar + + T = TypeVar('T') + Ts = TypeVarTuple('Ts') + + MyType = Tuple[int, T, *Ts] + + t: MyType[str, bool, float] + expr = t + """); + } + + // PY-53105 + public void testGenericVariadicAndGenericConsecutiveTypeAlias() { + doTest("tuple[int, str, list[str], dict[str, int]]", + """ + from typing import Tuple, TypeVarTuple, TypeVar + + T = TypeVar('T') + Ts = TypeVarTuple('Ts') + + MyType = Tuple[int, T, *Ts] + MyType1 = MyType[str, *Ts] + + t: MyType1[list[str], dict[str, int]] + expr = t + """); + } + + // PY-53105 + public void testGenericVariadicsIntersects() { + doTest("tuple[int, str, bool, list[str], dict[str, int]]", + """ + from typing import Tuple, TypeVarTuple, TypeVar + + T = TypeVar('T') + Ts = TypeVarTuple('Ts') + Ts1 = TypeVarTuple('Ts1') + + MyType = Tuple[int, T, *Ts] + MyType1 = MyType[str, bool, *Ts1] + + t: MyType1[list[str], dict[str, int]] + expr = t + """); + } + + // PY-53105 + public void testGenericVariadicsIntersectsSameName() { + doTest("tuple[int, str, bool, list[str], dict[str, int]]", + """ + from typing import Tuple, TypeVarTuple, TypeVar + + T = TypeVar('T') + Ts = TypeVarTuple('Ts') + + MyType = Tuple[int, T, *Ts] + MyType1 = MyType[str, bool, *Ts] + + t: MyType1[list[str], dict[str, int]] # first place \s + expr = t + """); + } + + // PY-53105 + public void testGenericVariadicsTupleUnpacking() { + doTest("tuple[int, str, bool, float]", + """ + from typing import Tuple, TypeVarTuple, TypeVar + Ts = TypeVarTuple('Ts') + MyType = Tuple[int, *Ts] + t: MyType[*tuple[str, bool, float]] + expr = t + """); + } + + // PY-53105 + public void testVariadicGenericMatchWithHomogeneousGenericVariadicAndOtherTypes() { + doTest("Array[*(Any, ...), int, str]",""" + from __future__ import annotations + + from typing import TypeVarTuple + from typing import Generic + from typing import Any + + Shape = TypeVarTuple("Shape") + + class Array(Generic[*Shape]): + ... + + y: Array[int, *tuple[Any, ...], int, str] = Array() + + def expect_variadic_array(x: Array[int, *Shape]) -> Array[*Shape]: + print(x) + + expr = expect_variadic_array(y) + """); + } + + // PY-53105 + public void testVariadicGenericMatchWithHomogeneousGenericVariadicAndOtherTypesPrefixSuffix() { + doTest("Array[*(Any, ...), int, float, str]",""" + from __future__ import annotations + + from typing import TypeVarTuple + from typing import TypeVar + from typing import Generic + from typing import Any + + Shape = TypeVarTuple("Shape") + T = TypeVar("T") + T1 = TypeVar("T1") + + class Array(Generic[*Shape]): + ... + + y: Array[int, float, *tuple[Any, ...], int, str] = Array() + + def expect_variadic_array(x: Array[int, T, *Shape, T1]) -> Array[*Shape, T, T1]: + print(x) + + expr = expect_variadic_array(y) + """); + } + + // PY-53105 + public void testVariadicGenericMatchWithHomogeneousGenericVariadicAmbiguousMatchActualGenericFirst() { + doTest("Array[int, str]",""" + from __future__ import annotations + + from typing import TypeVarTuple + from typing import TypeVar + from typing import Generic + from typing import Any + + Shape = TypeVarTuple("Shape") + T = TypeVar("T") + T1 = TypeVar("T1") + + class Array(Generic[*Shape]): + ... + + y: Array[int, *tuple[float, ...], int, str] = Array() + + def expect_variadic_array(x: Array[int, T, *Shape, T1]) -> Array[*Shape, T, T1]: + print(x) + + expr = expect_variadic_array(y) + """); + } + + // PY-53105 + public void testGenericVariadicsNotUnifiedBothAmbiguousMatch() { + doTest("Array[int, str]",""" + from __future__ import annotations + + from typing import TypeVarTuple + from typing import TypeVar + from typing import Generic + from typing import Any + + Shape = TypeVarTuple("Shape") + T = TypeVar("T") + T1 = TypeVar("T1") + + class Array(Generic[*Shape]): + ... + + y: Array[*tuple[int, ...], int, str] = Array() + + def expect_variadic_array(x: Array[int, T, *Shape, T1]) -> Array[*Shape, T, T1]: + print(x) + + expr = expect_variadic_array(y) + """); + } + + // PY-53105 + public void testGenericVariadicsNotUnifiedBothActualHomogeneousGenericFirst() { + doTest("Array[float, *(float, ...)]",""" + from __future__ import annotations + + from typing import TypeVarTuple + from typing import TypeVar + from typing import Generic + from typing import Any + + Shape = TypeVarTuple("Shape") + T = TypeVar("T") + + class Array(Generic[*Shape]): + ... + + y: Array[*tuple[float, ...]] = Array() + + def expect_variadic_array(x: Array[T, *Shape]) -> Array[T, *Shape]: + print(x) + + expr = expect_variadic_array(y) + """); + } + + // PY-53105 + public void testGenericVariadicsNotUnifiedBothActualHomogeneousGenericLast() { + doTest("Array[*(float, ...), float]",""" + from __future__ import annotations + + from typing import TypeVarTuple + from typing import TypeVar + from typing import Generic + from typing import Any + + Shape = TypeVarTuple("Shape") + T = TypeVar("T") + + class Array(Generic[*Shape]): + ... + + y: Array[*tuple[float, ...]] = Array() + + def expect_variadic_array(x: Array[*Shape, T]) -> Array[*Shape, T]: + print(x) + + expr = expect_variadic_array(y) + """); + } + + // PY-53105 + public void testGenericVariadicsNotUnifiedBothActualHomogeneousGenericsBothSides() { + doTest("Array[float, *(float, ...), float, float]",""" + from __future__ import annotations + + from typing import TypeVarTuple + from typing import TypeVar + from typing import Generic + from typing import Any + + Shape = TypeVarTuple("Shape") + T = TypeVar("T") + T1 = TypeVar("T1") + T2 = TypeVar("T2") + T3 = TypeVar("T3") + + class Array(Generic[*Shape]): + ... + + y: Array[*tuple[float, ...]] = Array() + + def expect_variadic_array(x: Array[T1, *Shape, T2, T3]) -> Array[T1, *Shape, T2, T3]: + print(x) + + expr = expect_variadic_array(y) + """); + } + + // PY-53105 + public void testGenericVariadicsNotUnifiedBothSameExpectedAndActual() { + doTest("Array[*Shape]",""" + from __future__ import annotations + + from typing import TypeVarTuple + from typing import TypeVar + from typing import Generic + from typing import Any + + Shape = TypeVarTuple("Shape") + Shape1 = TypeVarTuple("Shape1") + T = TypeVar("T") + T1 = TypeVar("T1") + + class Array(Generic[*Shape]): + ... + + y: Array[int, *Shape, str] = Array() + + def expect_variadic_array(x: Array[int, *Shape1, str]) -> Array[*Shape1]: + print(x) + + expr = expect_variadic_array(y) + """); + } + + // PY-53105 + public void testGenericVariadicsNotUnifiedBothExpectedExpand() { + doTest("Array[float, *Shape, list[str]]",""" + from __future__ import annotations + + from typing import TypeVarTuple + from typing import TypeVar + from typing import Generic + from typing import Any + + Shape = TypeVarTuple("Shape") + Shape1 = TypeVarTuple("Shape1") + T = TypeVar("T") + T1 = TypeVar("T1") + + class Array(Generic[*Shape]): + ... + + y: Array[int, float, *Shape, list[str], str] = Array() + + def expect_variadic_array(x: Array[int, *Shape1, str]) -> Array[*Shape1]: + print(x) + + expr = expect_variadic_array(y) + """); + } + + // PY-53105 + public void testGenericVariadicsNotUnifiedBothExpectedExpandTwoArguments() { + doTest("Array[float, bool, list[str]]",""" + from __future__ import annotations + + from typing import TypeVarTuple + from typing import TypeVar + from typing import Generic + from typing import Any + + Shape = TypeVarTuple("Shape") + Shape1 = TypeVarTuple("Shape1") + T = TypeVar("T") + T1 = TypeVar("T1") + + class Array(Generic[*Shape]): + ... + + a: Array[int, float, *Shape, list[str], str] = Array() + + def expect_variadic_arrays(x: Array[int, *Shape1, str], y: Array[int, float, bool, list[str], str]) -> Array[*Shape1]: + print(x, y) + + expr = expect_variadic_arrays(a, a) + """); + } + + public void testGenericVariadicsNotUnifiedBothExpectedExpandTwoArgumentsGenericVariadic() { + doTest("Array[float, float, *Shape142, list[str], list[str]]",""" + from __future__ import annotations + + from typing import TypeVarTuple + from typing import TypeVar + from typing import Generic + from typing import Any + + Shape = TypeVarTuple("Shape") + Shape1 = TypeVarTuple("Shape1") + T = TypeVar("T") + T1 = TypeVar("T1") + + class Array(Generic[*Shape]): + ... + + a: Array[int, float, *Shape, list[str], str] = Array() + + def expect_variadic_arrays(x: Array[int, *Shape1, str], y: Array[int, float, *Shape1, list[str], str]) -> Array[*Shape1]: + print(x, y) + + expr = expect_variadic_arrays(a, a) + """); + } + + public void testGenericVariadicsNotUnifiedBothExpectedExpandTwoDifferentArgumentsGenericVariadic() { + doTest("Array[float, *Shape, bool, list[str]] | Array[*Shape]",""" + from __future__ import annotations + + from typing import TypeVarTuple + from typing import TypeVar + from typing import Generic + from typing import Any + + Shape = TypeVarTuple("Shape") + Shape1 = TypeVarTuple("Shape1") + T = TypeVar("T") + T1 = TypeVar("T1") + + class Array(Generic[*Shape]): + ... + + a: Array[int, float, *Shape, list[str], str] = Array() + + def expect_variadic_arrays(x: Array[int, *Shape1, str], y: Array[int, float, *Shape, bool, list[str], str]) -> Array[*Shape1] | Array[*Shape]: + print(x, y) + + expr = expect_variadic_arrays(a, a) + """); + } + + // PY-53105 + public void testGenericVariadicsNotUnifiedBothExpectedExpandNotExactLeft() { + doTest("Array[*(Any, ...), list[str]]",""" + from __future__ import annotations + + from typing import TypeVarTuple + from typing import TypeVar + from typing import Generic + from typing import Any + + Shape = TypeVarTuple("Shape") + Shape1 = TypeVarTuple("Shape1") + T = TypeVar("T") + T1 = TypeVar("T1") + + class Array(Generic[*Shape]): + ... + + y: Array[int, *Shape, list[str], str] = Array() + + def expect_variadic_array(x: Array[int, float, *Shape1, str]) -> Array[*Shape1]: + print(x) + + expr = expect_variadic_array(y) + """); + } + + // PY-53105 + public void testGenericVariadicsNotUnifiedBothExpectedExpandNotExactRight() { + doTest("Array[float, *(Any, ...)]",""" + from __future__ import annotations + + from typing import TypeVarTuple + from typing import TypeVar + from typing import Generic + from typing import Any + + Shape = TypeVarTuple("Shape") + Shape1 = TypeVarTuple("Shape1") + T = TypeVar("T") + T1 = TypeVar("T1") + + class Array(Generic[*Shape]): + ... + + y: Array[int, float, *Shape, str] = Array() + + def expect_variadic_array(x: Array[int, *Shape1, int, str]) -> Array[*Shape1]: + print(x) + + expr = expect_variadic_array(y) + """); + } + + // PY-53105 + public void testGenericVariadicsNotUnifiedBothActualSwallowAllExpected() { + doTest("Array[*Shape1]",""" + from __future__ import annotations + + from typing import TypeVarTuple + from typing import TypeVar + from typing import Generic + from typing import Any + + Shape = TypeVarTuple("Shape") + Shape1 = TypeVarTuple("Shape1") + T = TypeVar("T") + T1 = TypeVar("T1") + + class Array(Generic[*Shape]): + ... + + y: Array[*Shape] = Array() + + def expect_variadic_array(x: Array[int, *Shape1, str]) -> Array[*Shape1]: + print(x) + + expr = expect_variadic_array(y) + """); + } + + // PY-53105 + public void testVariadicGenericClassOverloadedMethods() { + doTest("Array[str, int]", """ + from __future__ import annotations + + from typing import TypeVarTuple + from typing import TypeVar + from typing import Generic + from typing import overload + + Shape = TypeVarTuple("Shape") + Axis1 = TypeVar("Axis1") + Axis2 = TypeVar("Axis2") + Axis3 = TypeVar("Axis3") + + + class Array(Generic[*Shape]): + @overload + def transpose(self: Array[Axis1, Axis2]) -> Array[Axis2, Axis1]: ... + + @overload + def transpose(self: Array[Axis1, Axis2, Axis3]) -> Array[Axis3, Axis2, Axis1]: ... + + def transpose(self): ... + + + a: Array[int, str] = Array() + + expr = a.transpose() + """); + } + + // PY-53105 + public void testVariadicGenericClassOverloadedMethodsSecondMethod() { + doTest("Array[list[int], str, int]", """ + from __future__ import annotations + + from typing import TypeVarTuple + from typing import TypeVar + from typing import Generic + from typing import overload + + Shape = TypeVarTuple("Shape") + Axis1 = TypeVar("Axis1") + Axis2 = TypeVar("Axis2") + Axis3 = TypeVar("Axis3") + + + class Array(Generic[*Shape]): + @overload + def transpose(self: Array[Axis1, Axis2]) -> Array[Axis2, Axis1]: ... + + @overload + def transpose(self: Array[Axis1, Axis2, Axis3]) -> Array[Axis3, Axis2, Axis1]: ... + + def transpose(self): ... + + + a: Array[int, str, list[int]] = Array() + + expr = a.transpose() + """); + } + public void testUnresolvedReturnTypeNotOverridenByAncestorAnnotation() { doTest("Any", """ @@ -2290,6 +3188,23 @@ public class PyTypingTest extends PyTestCase { """); } + // PY-53105 + public void testGenericVariadicMethodCallUnification() { + doTest("tuple[int, LiteralString, float]", """ + from typing import Generic, TypeVarTuple, Tuple + + Ts = TypeVarTuple("Ts") + + class Box(Generic[*Ts]): + def __init__(self, value: Tuple[*Ts]) -> None: + self.value = value + def get(self): + return self.value + + box = Box((42, 'a', 3.3)) + expr = box.get()"""); + } + public void testSingleTypeVarSpecifiedOnInheritance() { doTest("str", """ from typing import Generic, TypeVar @@ -2309,6 +3224,27 @@ public class PyTypingTest extends PyTestCase { expr = extract(box)"""); } + // PY-53105 + public void testSingleTypeVarTupleSpecifiedOnInheritance() { + doTest("tuple[str, int]", """ + from typing import Generic, TypeVarTuple, Tuple + + Ts = TypeVarTuple("Ts") + + class Box(Generic[*Ts]): + pass + + class StrBox(Box[str, int]): + pass + + def extract(b: Box[*Ts]) -> Tuple[*Ts]: + pass + + box = StrBox() + expr = extract(box) + """); + } + public void testPartialTypeVarSpecializationOnInheritanceInherited() { doTest("str", """ @@ -2418,6 +3354,30 @@ public class PyTypingTest extends PyTestCase { expr = func(box)"""); } + // PY-53105 + public void testTypeVarTupleSpecializedOnInheritanceExtraTypeVarAdded() { + doTest("tuple[str, int]", + """ + from typing import Generic, TypeVarTuple, Tuple + + Ts1 = TypeVarTuple('Ts1') + Ts2 = TypeVarTuple('Ts2') + Ts3 = TypeVarTuple('Ts3') + + class Box(Generic[*Ts1]): + pass + + class StrBoxWithExtra(Box[str, int], Generic[*Ts2]): + def __init__(self, extra: Tuple[*Ts2]): + self.extra = extra + + def func(b: Box[*Ts3]) -> Tuple[*Ts3]: + pass + + box = StrBoxWithExtra((42, 'a', 3.3)) + expr = func(box)"""); + } + public void testGenericClassSpecializesInheritedParameterAndAddsNewOne() { doTest("StrBoxWithExtra[int]", """ @@ -2436,6 +3396,25 @@ public class PyTypingTest extends PyTestCase { expr = StrBoxWithExtra(42)"""); } + // PY-53105 + public void testGenericVariadicClassSpecializesInheritedParameterAndAddsNewOne() { + doTest("StrBoxWithExtra[int, LiteralString, float]", + """ + from typing import Generic, TypeVarTuple, Tuple + + Ts1 = TypeVarTuple('Ts1') + Ts2 = TypeVarTuple('Ts2') + + class Box(Generic[*Ts1]): + pass + + class StrBoxWithExtra(Box[str], Generic[*Ts2]): + def __init__(self, extra: Tuple[*Ts2]): + self.extra = extra + + expr = StrBoxWithExtra((42, 'a', 3.3))"""); + } + public void testGenericSelfSpecializationInOverloadedConstructor() { doTest("Pair[int, int]", """ @@ -2498,6 +3477,25 @@ public class PyTypingTest extends PyTestCase { expr = receiver.get()"""); } + // PY-53105 + public void testWeakUnionTypeOfOfGenericVariadicMethodCallReceiver() { + doTest("tuple[str, int, float]", + """ + from typing import Any, Generic, TypeVarTuple, Tuple + + Ts = TypeVarTuple("Ts") + + class Box(Generic[*Ts]): + def get(self) -> Tuple[*Ts]: + pass + + class StrBox(Box[str, int, float]): + pass + + receiver: Any | int | StrBox = ... + expr = receiver.get()"""); + } + public void testGenericClassTypeHintedInDocstrings() { doTest("int", """ @@ -2516,6 +3514,25 @@ public class PyTypingTest extends PyTestCase { expr = c.get()"""); } + // PY-53105 + public void testGenericVariadicClassTypeHintedInDocstrings() { + doTest("tuple[int, LiteralString, float]", + """ + from typing import Generic, TypeVar, TypeVarTuple, Tuple + + Ts = TypeVarTuple('Ts') + + class User1(Generic[*Ts]): + def __init__(self, x: Tuple[*Ts]): + self.x = x + + def get(self) -> Tuple[*Ts]: + return self.x + + c = User1((1, '2', 3.3)) + expr = c.get()"""); + } + public void testIterOnListOfListsResult() { doTest("Iterator[list[int]]", "expr = iter([[1, 2, 3]])"); @@ -2565,6 +3582,27 @@ public class PyTypingTest extends PyTestCase { expr = dec('foo')(func)"""); } + // PY-53105 + public void testGenericVariadicDecoratorWithArgumentCalledAsFunction() { + doTest("(tuple[str, int]) -> tuple[int, str, float]", + """ + from typing import Callable, TypeVar, TypeVarTuple, Tuple + + Ss = TypeVarTuple('Ss') + Ts = TypeVarTuple('Ts') + + def dec(ts: Tuple[*Ts]): + def g(fun: Callable[[], Tuple[*Ss]]) -> Callable[[*Ts], Tuple[*Ss]]: + ... + + return g + + def func() -> Tuple[int, str, float]: + ... + + expr = dec(('foo', 42))(func)"""); + } + public void testGenericParameterOfExpectedCallable() { doTest("int", """ @@ -2587,6 +3625,29 @@ public class PyTypingTest extends PyTestCase { expr = f(g)"""); } + // PY-53105 + public void testGenericVariadicParameterOfExpectedCallable() { + doTest("tuple[int, str, float]", + """ + from typing import Callable, Generic, TypeVar, TypeVarTuple, Tuple + + Ts = TypeVarTuple('Ts') + + class Super(Generic[*Ts]): + pass + + class Sub(Super[*Ts]): + pass + + def f(x: Callable[[Sub[*Ts]], None]) -> Tuple[*Ts]: + pass + + def g(x: Super[int, str, float]): + pass + + expr = f(g)"""); + } + // PY-53522 public void testGenericIteratorParameterizedWithAnotherGeneric() { doTest("Entry[LiteralString]", @@ -2609,6 +3670,28 @@ public class PyTypingTest extends PyTestCase { """); } + // PY-53105 + public void testGenericVariadicIteratorParameterizedWithAnotherGenericVariadic() { + doTest("Entry[LiteralString, int, float]", + """ + from typing import Iterator, Generic, Tuple, TypeVarTuple + + Ts = TypeVarTuple("Ts") + + class Entry(Generic[*Ts]): + pass + + class MyIterator(Iterator[Entry[*Ts]]): + def __next__(self) -> Entry[*Ts]: ... + + def iter_entries(path: Tuple[*Ts]) -> MyIterator[*Ts]: ... + + def main() -> None: + for x in iter_entries(("some path", 1, 1.1)): + expr = x + """); + } + // PY-53522 public void testGenericParameterizedWithGeneric() { doTest("list[int]", @@ -2629,6 +3712,29 @@ public class PyTypingTest extends PyTestCase { """); } + // PY-53105 + public void testGenericVariadicParameterizedWithGenericVariadic() { + doTest("tuple[tuple[int, str]]", + """ + from typing import Generic, TypeVar, TypeVarTuple, Tuple + + Ts = TypeVarTuple('Ts') + + + class Box(Generic[*Ts]): + def get(self) -> Tuple[*Ts]: + pass + + + class ListBox(Box[Tuple[*Ts]]): + pass + + + xs: ListBox[int, str] = ... + expr = xs.get() + """); + } + // PY-52656 public void testClassInheritsGenericToOrderTypeParameters() { doTest("str", diff --git a/python/testSrc/com/jetbrains/python/inspections/Py3TypeCheckerInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/Py3TypeCheckerInspectionTest.java index 6963bf748de5..6608f36addb6 100644 --- a/python/testSrc/com/jetbrains/python/inspections/Py3TypeCheckerInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/Py3TypeCheckerInspectionTest.java @@ -1256,6 +1256,404 @@ public class Py3TypeCheckerInspectionTest extends PyInspectionTestCase { """); } + // PY-53105 + public void testVariadicGenericInFunction() { + doTestByText("from typing import Tuple, TypeVarTuple, TypeVar\n" + + "\n" + + "T = TypeVar('T')\n" + + "Ts = TypeVarTuple('Ts')\n" + + "\n" + + "\n" + + "def foo(x: T, y: Tuple[*Ts]):\n" + + " pass\n" + + "\n" + + "\n" + + "foo(10, (1, '1', [1]))\n"); + } + + // PY-53105 + public void testVariadicGenericArgumentByCallableInFunction() { + doTestByText("from typing import Callable, TypeVarTuple, Tuple\n" + + "\n" + + "Ts = TypeVarTuple('Ts')\n" + + "\n" + + "\n" + + "def foo(a: int, f: Callable[[*Ts], None], args: Tuple[*Ts]) -> None: ...\n" + + "def bar(a: int, b: str) -> None: ...\n" + + "\n" + + "\n" + + "foo(1, bar, args=(0, 'foo'))\n" + + "\n" + + "foo(1, bar, args=('foo', 0))\n"); + } + + // PY-53105 + public void testVariadicGenericCheckCallableInFunction() { + doTestByText("from typing import TypeVar, TypeVarTuple, Callable, Tuple\n" + + "\n" + + "T = TypeVar('T')\n" + + "Ts = TypeVarTuple('Ts')\n" + + "\n" + + "\n" + + "def foo(f: Callable[[int, *Ts, T], Tuple[T, *Ts]]) -> None: ...\n" + + "\n" + + "\n" + + "def ok1(a: int, b: str, c: bool, d: list[int]) -> Tuple[list[int], str, bool]: ...\n" + + "def ok2(a: int, b: str) -> Tuple[str]: ...\n" + + "\n" + + "\n" + + "foo(ok1)\n" + + "foo(ok2)\n" + + "\n" + + "\n" + + "def err1(a: int, b: str, c: bool, d: list[int]) -> Tuple[list[int], str, str]: ...\n" + + "def err2(a: int, b: str) -> Tuple[str, str]: ...\n" + + "\n" + + "\n" + + "foo( tuple[list[int], str, bool]' (matched generic type '(int, *Ts, T) -> tuple[T, *Ts]'), got '(a: int, b: str, c: bool, d: list[int]) -> tuple[list[int], str, str]' instead\">err1)\n" + + "foo( tuple[str]' (matched generic type '(int, *Ts, T) -> tuple[T, *Ts]'), got '(a: int, b: str) -> tuple[str, str]' instead\">err2)\n"); + } + + // PY-53105 + public void testVariadicGenericTwoTsInFunction() { + doTestByText("from typing import TypeVarTuple, Generic\n" + + "\n" + + "Ts = TypeVarTuple('Ts')\n" + + "\n" + + "\n" + + "class Array(Generic[*Ts]):\n" + + " ...\n" + + "\n" + + "\n" + + "def foo(x: Array[*Ts], y: Array[*Ts]) -> Array[*Ts]:\n" + + " ...\n" + + "\n" + + "\n" + + "x: Array[int]\n" + + "y: Array[str]\n" + + "z: Array[int, str]\n" + + "\n" + + "foo(x, x)\n" + + "\n" + + "foo(x, y)\n" + + "foo(x, z)\n"); + } + + // PY-53105 + public void testVariadicGenericUnboundTupleInFunction() { + doTestByText("from typing import Generic, TypeVarTuple, Tuple, Any\n" + + "\n" + + "Ts = TypeVarTuple('Ts')\n" + + "\n" + + "\n" + + "class Array(Generic[*Ts]):\n" + + " def __init__(self, shape: Tuple[*Ts]):\n" + + " ...\n" + + "\n" + + "\n" + + "def foo(x: Array[int, *Tuple[Any, ...], str]) -> None:\n" + + " ...\n" + + "\n" + + "\n" + + "x: Array[int, list[str], bool, str]\n" + + "foo(x)\n" + + "\n" + + "y: Array[int, str]\n" + + "foo(y)\n" + + "\n" + + "z: Array[int]\n" + + "foo(z)\n" + + "\n" + + "t: Array[str]\n" + + "foo(t)\n" + + "\n" + + "k: Array[int, int]\n" + + "foo(k)\n"); + } + + // PY-53105 + public void testVariadicGenericStarArgsNamedParameters() { + doTestByText("from typing import Tuple, TypeVarTuple\n" + + "\n" + + "Ts = TypeVarTuple('Ts')\n" + + "\n" + + "\n" + + "def foo(a: str, *args: *Tuple[*Ts, int], b: str, c: bool) -> None: ...\n" + + "\n" + + "\n" + + "foo('', 1, True, [1], 42, b='', c=True)\n" + + "foo('', 42, b='', c=True)\n" + + "foo('', True, 42, c=True, b='')\n" + + "\n" + + "foo('', b='', c=True)\n" + + "foo('', '', b='', c=True)\n" + + "foo('', '', [False], b='', c=True)\n" + + "foo('', '', '', '', 1.1, b='', c=True)\n"); + } + + // PY-53105 + public void testVariadicGenericStarArgsTupleAndUnpackedTuple() { + doTestByText("from typing import Tuple, TypeVarTuple\n" + + "\n" + + "Ts = TypeVarTuple('Ts')\n" + + "\n" + + "\n" + + "def foo(a: Tuple[*Ts], *args: *Tuple[str, *Ts, int], b: str) -> None: ...\n" + + "\n" + + "\n" + + "foo(('', 1), '', '', 1, 1, b='')\n" + + "foo((1,1), '', 1, 1, 1, b='')\n" + + "foo(('',), '', '', 1, b='')\n" + + "foo((), '', 1, b='')\n" + + "foo(([], {}), '', [], {}, 1, b='')\n" + + "\n" + + "foo(('', 1), b='')\n" + + "foo(('', 1), '', '', '', 1, b='')\n" + + "foo((1,1), '', 1, 1, b='')\n" + + "foo(('',), '', 1, 1, b='')\n" + + "x: Any\n" + + "foo((), '', 42, x, b='')\n" + + "foo(([], {}), '', [], {}, b='')\n"); + } + + // PY-53105 + public void testVariadicGenericStarArgsOfVariadicGeneric() { + doTestByText("from typing import Tuple, TypeVarTuple\n" + + "\n" + + "Ts = TypeVarTuple('Ts')\n" + + "\n" + + "\n" + + "def foo(*args: Tuple[*Ts]): ...\n" + + "\n" + + "\n" + + "foo((0,), (1,))\n" + + "foo((0,), (1, 2))\n" + + "foo((0,), ('1',))\n"); + } + + // PY-53105 + public void testVariadicGenericStarArgsOfVariadicGenericPrefixSuffix() { + doTestByText("from typing import Tuple, TypeVarTuple\n" + + "\n" + + "Ts = TypeVarTuple('Ts')\n" + + "\n" + + "\n" + + "def foo(*args: Tuple[int, *Ts, str]): ...\n" + + "\n" + + "\n" + + "foo((0,''), (1,''))\n" + + "foo((0, [1], {}, ''), (1, [1], {}, ''))\n" + + "\n" + + "foo((0, [1], {}, ''), (1, [1], ''))\n" + + "foo((0, [1], {}, ''), (1, ['1'], {}, ''))\n" + + "foo((0, [1], {}, ''), (1, ''))\n" + + "foo((0, [1], {}, ''), (1, [1], {}, 1))\n"); + } + + // PY-53105 + public void testVariadicGenericStarArgsPrefixSuffix() { + doTestByText("from typing import Tuple, TypeVarTuple\n" + + "\n" + + "Ts = TypeVarTuple('Ts')\n" + + "\n" + + "\n" + + "def foo(*args: *Tuple[int, *Tuple[str, ...], str]) -> None: ...\n" + + "\n" + + "\n" + + "foo(1, '')\n" + + "foo(1, '', '')\n" + + "foo(1, '', '', '', '', '', '')\n" + + "\n" + + "foo(1, 1)\n" + + "foo('', '')\n" + + "foo(1, '', '', 1, '', '', '')\n"); + } + + // PY-53105 + public void testVariadicGenericStarArgsUnboundTuple() { + doTestByText("from typing import Tuple\n" + + "\n" + + "\n" + + "def foo(*args: *Tuple[int, ...]) -> None: ...\n" + + "\n" + + "\n" + + "foo()\n" + + "foo(1)\n" + + "foo(1, 2, 3)\n" + + "\n" + + "foo('')\n" + + "foo(1, '')\n"); + } + + // PY-53105 + public void testVariadicGenericMatchWithHomogeneousGenericVariadic() { + doTestByText(""" + from __future__ import annotations + + from typing import TypeVarTuple + from typing import Generic + from typing import Any + + Shape = TypeVarTuple("Shape") + + class Array(Generic[*Shape]): + ... + + y: Array[*tuple[Any, ...]] = Array() + + def expect_variadic_array(x: Array[int, *Shape]) -> None: + print(x) + + expect_variadic_array(y) + """); + } + + // PY-53105 + public void testVariadicGenericMatchWithHomogeneousGenericVariadicAndOtherTypes() { + doTestByText(""" + from __future__ import annotations + + from typing import TypeVarTuple + from typing import Generic + from typing import Any + + Shape = TypeVarTuple("Shape") + + class Array(Generic[*Shape]): + ... + + y: Array[*tuple[Any, ...], int, str] = Array() + + def expect_variadic_array(x: Array[int, *Shape]) -> None: + print(x) + + expect_variadic_array(y) + """); + } + + // PY-53105 + public void testVariadicGenericCheckTypeAliasesMissingParameter() { + doTestByText(""" + from typing import TypeVarTuple + from typing import TypeVar + from typing import Generic + from typing import NewType + + Shape = TypeVarTuple("Shape") + Height = NewType("Height", int) + Width = NewType("Width", int) + DType = TypeVar("DType") + + + class Array(Generic[DType, *Shape]): + ... + + + Float32Array = Array[float, *Shape] + + + def takes_float_array_of_specific_shape(arr: Float32Array[Height, Width]): ... + + + y: Float32Array[Height] = Array() + takes_float_array_of_specific_shape(y) + """); + } + + // PY-53105 + public void testVariadicGenericCheckTypeAliasesRedundantParameter() { + doTestByText(""" + from typing import TypeVarTuple + from typing import TypeVar + from typing import Generic + from typing import NewType + + Shape = TypeVarTuple("Shape") + Height = NewType("Height", int) + Width = NewType("Width", int) + DType = TypeVar("DType") + + + class Array(Generic[DType, *Shape]): + ... + + + Float32Array = Array[float, *Shape] + + + def takes_float_array_of_specific_shape(arr: Float32Array[Height]): ... + + + y: Float32Array[Height, Width] = Array() + takes_float_array_of_specific_shape(y) + """); + } + + // PY-53105 + public void testVariadicGenericEmpty() { + doTestByText(""" + from typing import TypeVarTuple + + Ts = TypeVarTuple("Ts") + + IntTuple = tuple[int, *Ts] + + c: IntTuple[()] = (1, "") + """); + } + + //// PY-53611 + //public void testTypedDictRequiredNotRequiredKeys() { + // runWithLanguageLevel(LanguageLevel.getLatest(), + // () -> doTestByText(""" + // from typing import TypedDict + // from typing_extensions import Required, NotRequired + // class WithTotalFalse(TypedDict, total=False): + // x: Required[int] + // class WithTotalTrue(TypedDict, total=True): + // x: NotRequired[int] + // class WithoutTotal(TypedDict): + // x: NotRequired[int] + // class WithoutTotalWithExplicitRequired(TypedDict): + // x: Required[int] + // y: NotRequired[int] + // AlternativeSyntax = TypedDict("AlternativeSyntax", {'x': NotRequired[int]}) + // with_total_false: WithTotalFalse = {} + // with_total_true: WithTotalTrue = {} + // without_total: WithoutTotal = {} + // without_total_with_explicit_required: WithoutTotalWithExplicitRequired = {} + // alternative_syntax: AlternativeSyntax = {} + // """)); + //} + + //// PY-53611 + //public void testTypedDictRequiredNotRequiredEquivalence() { + // runWithLanguageLevel(LanguageLevel.getLatest(), this::doTest); + //} + // + //// PY-53611 + //public void testTypedDictRequiredNotRequiredMixedWithAnnotated() { + // runWithLanguageLevel(LanguageLevel.getLatest(), + // () -> doTestByText(""" + // from typing_extensions import TypedDict, Required, NotRequired, Annotated + // class A(TypedDict): + // x: Annotated[NotRequired[int], 'Some constraint'] + // def f(a: A): + // pass + // f({}) + // class B(TypedDict, total=False): + // x: Annotated[Required[int], 'Some constraint'] + // def g(b: B): + // pass + // g({}) + // """)); + //} + // + //// PY-53611 + //public void testTypingRequiredTypeSpecificationsMultiFile() { + // doMultiFileTest(); + //} + // PY-56785 public void testTypingSelfNoInspectionReturnSelfMethod() { doTestByText(""" diff --git a/python/testSrc/com/jetbrains/python/inspections/PyTypeHintsInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyTypeHintsInspectionTest.java index c92066d9f0d3..4a961b7e5da5 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyTypeHintsInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyTypeHintsInspectionTest.java @@ -1349,6 +1349,51 @@ public class PyTypeHintsInspectionTest extends PyInspectionTestCase { """); } + // PY-53105 + public void testNoVariadicGenericErrorInClassDeclaration() { + doTestByText(""" + from typing import Generic, TypeVarTuple + + Shape = TypeVarTuple('Shape') + + + class Array(Generic[*Shape]): + ... + """); + } + + // PY-53105 + public void testTypeVarTupleNameAsLiteral() { + doTestByText("from typing import TypeVarTuple\n" + + "\n" + + "name = 'Ts'\n" + + "Ts = TypeVarTuple(name)\n" + + "Ts1 = TypeVarTuple('Ts1')"); + } + + // PY-53105 + public void testTypeVarTupleNameAndTargetNameEquality() { + doTestByText("from typing import TypeVarTuple\n" + + "\n" + + "Ts = TypeVarTuple('T')\n" + + "Ts1 = TypeVarTuple('Ts1')"); + } + + // PY-53105 + public void testTypeVarTupleMoreThanOneUnpacking() { + doTestByText(""" + from typing import TypeVarTuple + from typing import Generic + + Ts1 = TypeVarTuple("Ts1") + Ts2 = TypeVarTuple("Ts2") + + + class Array(Generic[*Ts1, *Ts2]): + ... + """); + } + @NotNull @Override protected Class getInspectionClass() { diff --git a/python/testSrc/com/jetbrains/python/quickFixes/PyReplaceStarByUnpackQuickFixTest.kt b/python/testSrc/com/jetbrains/python/quickFixes/PyReplaceStarByUnpackQuickFixTest.kt new file mode 100644 index 000000000000..af150951865d --- /dev/null +++ b/python/testSrc/com/jetbrains/python/quickFixes/PyReplaceStarByUnpackQuickFixTest.kt @@ -0,0 +1,32 @@ +// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python.quickFixes + +import com.jetbrains.python.PyPsiBundle +import com.jetbrains.python.PyQuickFixTestCase +import com.jetbrains.python.inspections.PyCompatibilityInspection +import com.jetbrains.python.psi.LanguageLevel + +class PyReplaceStarByUnpackQuickFixTest: PyQuickFixTestCase() { + // PY-53105 + fun testTypeVarTupleAfterTypeVar() { + doTest(LanguageLevel.PYTHON310) + } + + // PY-53105 + fun testTypeVarTupleOnly() { + doTest(LanguageLevel.PYTHON310) + } + + fun doTest(languageLevel: LanguageLevel) { + runWithLanguageLevel(languageLevel) { + val testFileName = getTestName(true) + myFixture.enableInspections(PyCompatibilityInspection::class.java) + myFixture.configureByFile("$testFileName.py") + myFixture.checkHighlighting(false, false, false) + val intentionAction = myFixture.getAvailableIntention(PyPsiBundle.message("QFIX.replace.star.by.unpack")) + assertNotNull(intentionAction) + myFixture.launchAction(intentionAction!!) + myFixture.checkResultByFile(testFileName + "_after.py", true) + } + } +} \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/quickFixes/PyUnpackTypeVarTupleQuickFixTest.kt b/python/testSrc/com/jetbrains/python/quickFixes/PyUnpackTypeVarTupleQuickFixTest.kt new file mode 100644 index 000000000000..ab1a12673180 --- /dev/null +++ b/python/testSrc/com/jetbrains/python/quickFixes/PyUnpackTypeVarTupleQuickFixTest.kt @@ -0,0 +1,31 @@ +package com.jetbrains.python.quickFixes + +import com.jetbrains.python.PyPsiBundle +import com.jetbrains.python.PyQuickFixTestCase +import com.jetbrains.python.inspections.PyTypeHintsInspection +import com.jetbrains.python.psi.LanguageLevel + +class PyUnpackTypeVarTupleQuickFixTest : PyQuickFixTestCase() { + // PY-53105 + fun testUnpackWithTypingExtensionUnpackOlderVersions() { + doTest(LanguageLevel.PYTHON310) + } + + // PY-53105 + fun testUnpackWithStarExpression() { + doTest() + } + + fun doTest(languageLevel: LanguageLevel = LanguageLevel.getLatest()) { + runWithLanguageLevel(languageLevel) { + val testFileName = getTestName(true) + myFixture.enableInspections(PyTypeHintsInspection::class.java) + myFixture.configureByFile("$testFileName.py") + myFixture.checkHighlighting(false, false, false) + val intentionAction = myFixture.getAvailableIntention(PyPsiBundle.message("QFIX.NAME.unpack.type.var.tuple")) + assertNotNull(intentionAction) + myFixture.launchAction(intentionAction!!) + myFixture.checkResultByFile(testFileName + "_after.py", true) + } + } +} \ No newline at end of file