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 1d77c3ba84a8..3c065d65f70f 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 @@ -726,6 +726,19 @@ public final class PyTypingTypeProvider extends PyTypeProviderWithCustomContext< .toList(); } + @NotNull + private static List collectTypeParametersFromTypeAliasStatement(@NotNull PyTypeAliasStatement typeAliasStatement, @NotNull Context context) { + PyTypeParameterList typeParameterList = typeAliasStatement.getTypeParameterList(); + if (typeParameterList != null) { + List typeParameters = typeParameterList.getTypeParameters(); + return StreamEx.of(typeParameters) + .map(typeParameter -> getTypeParameterTypeFromTypeParameter(typeParameter, context)) + .nonNull() + .toList(); + } + return Collections.emptyList(); + } + public static boolean isGeneric(@NotNull PyWithAncestors descendant, @NotNull TypeEvalContext context) { if (descendant instanceof PyClass pyClass && pyClass.getTypeParameterList() != null || descendant instanceof PyClassType pyClassType && pyClassType.getPyClass().getTypeParameterList() != null) { @@ -835,6 +848,10 @@ public final class PyTypingTypeProvider extends PyTypeProviderWithCustomContext< if (typeFromParenthesizedExpression != null) { return typeFromParenthesizedExpression; } + final PyType parameterizedTypeFromTypeAlias = getParameterizedTypeFromTypeAlias(alias, typeHint, resolved, context); + if (parameterizedTypeFromTypeAlias != null) { + return Ref.create(parameterizedTypeFromTypeAlias); + } final PyType unionType = getUnionType(resolved, context); if (unionType != null) { return Ref.create(unionType); @@ -1852,6 +1869,111 @@ public final class PyTypingTypeProvider extends PyTypeProviderWithCustomContext< return types; } + @Nullable + public static PyType tryParameterizeClassWithDefaults(@NotNull PyClassType classType, + @NotNull PyExpression anchor, + boolean toInstance, + @NotNull TypeEvalContext context) { + return staticWithCustomContext(context, customContext -> parameterizeClassWithDefaults(classType, anchor, toInstance, customContext)); + } + + @Nullable + private static PyType parameterizeClassWithDefaults(@NotNull PyClassType classType, + @NotNull PyExpression anchor, + boolean toInstance, + @NotNull Context context) { + PyClass pyClass = classType.getPyClass(); + if (isGeneric(pyClass, context.getTypeContext())) { + PyCollectionType genericDefinitionType = PyTypeChecker.findGenericDefinitionType(pyClass, context.getTypeContext()); + if (genericDefinitionType != null && ContainerUtil.exists(genericDefinitionType.getElementTypes(), + t -> t instanceof PyTypeParameterType typeParameterType && + typeParameterType.getDefaultType() != null)) { + PyType parameterizedType; + if (anchor instanceof PySubscriptionExpression subscriptionExpression) { + List indexTypes = getIndexTypes(subscriptionExpression, context); + parameterizedType = PyTypeChecker.parameterizeType(genericDefinitionType, indexTypes, context.myContext); + } + else { + List typeParameters = collectTypeParameters(genericDefinitionType.getPyClass(), context); + parameterizedType = PyTypeChecker.trySubstituteByDefaultsOnly(genericDefinitionType, typeParameters, true, context.getTypeContext()); + } + if (parameterizedType instanceof PyCollectionType collectionType) { + return toInstance ? collectionType.toInstance() : collectionType.toClass(); + } + } + } + return null; + } + + @Nullable + private static PyType getParameterizedTypeFromTypeAlias(@Nullable PyQualifiedNameOwner alias, + @NotNull PsiElement typeHint, + @NotNull PsiElement element, + @NotNull Context context) { + if (alias instanceof PyTypeAliasStatement typeAliasStatement) { + return getParameterizedTypeFromTypeAliasStatement(typeAliasStatement, typeHint, element, context); + } + if (alias != null && element instanceof PyExpression assignedExpression) { + if (typeHint instanceof PySubscriptionExpression subscriptionExpr) { + List indexTypes = getIndexTypes(subscriptionExpr, context); + PyType assignedType = Ref.deref(getType(assignedExpression, context)); + if (assignedType != null) { + return PyTypeChecker.parameterizeType(assignedType, indexTypes, context.myContext); + } + } + if (typeHint instanceof PyReferenceExpression) { + PyType expressionType = Ref.deref(getType(assignedExpression, context)); + if (expressionType != null && !(expressionType instanceof PyTypeParameterType)) { + List typeAliasTypeParams = + PyTypeChecker.collectGenerics(expressionType, context.getTypeContext()).getAllTypeParameters(); + if (!typeAliasTypeParams.isEmpty()) { + return PyTypeChecker.trySubstituteByDefaultsOnly(expressionType, typeAliasTypeParams, false, context.getTypeContext()); + } + } + if (expressionType instanceof PyCollectionType) { + return expressionType; + } + } + } + return null; + } + + @Nullable + private static PyType getParameterizedTypeFromTypeAliasStatement(@NotNull PyTypeAliasStatement typeAliasStatement, + @NotNull PsiElement typeHint, + @NotNull PsiElement element, + @NotNull Context context) { + if (element instanceof PyExpression expression) { + if (typeHint instanceof PySubscriptionExpression subscriptionExpression) { + final List indexTypes = getIndexTypes(subscriptionExpression, context); + PyType assignedType = Ref.deref(getType(expression, context)); + List typeParamsFromAliasStmt = collectTypeParametersFromTypeAliasStatement(typeAliasStatement, context); + if (assignedType != null && !typeParamsFromAliasStmt.isEmpty()) { + + PyTypeChecker.GenericSubstitutions substitutions = + PyTypeChecker.mapTypeParametersToSubstitutions(new PyTypeChecker.GenericSubstitutions(), + typeParamsFromAliasStmt, + indexTypes, + Option.MAP_UNMATCHED_EXPECTED_TYPES_TO_ANY); + + if (substitutions == null) return null; + var substitutionsWithDefaults = PyTypeChecker.getSubstitutionsWithDefaults(substitutions); + return PyTypeChecker.substitute(assignedType, substitutionsWithDefaults, context.getTypeContext()); + } + } + else if (typeHint instanceof PyReferenceExpression) { + List typeAliasTypeParams = collectTypeParametersFromTypeAliasStatement(typeAliasStatement, context); + if (!typeAliasTypeParams.isEmpty()) { + PyType operandType = Ref.deref(getType(expression, context)); + if (operandType != null) { + return PyTypeChecker.trySubstituteByDefaultsOnly(operandType, typeAliasTypeParams, false, context.getTypeContext()); + } + } + } + } + return null; + } + @Nullable private static PyType getParameterizedType(@NotNull PsiElement element, @NotNull Context context) { if (element instanceof PySubscriptionExpression subscriptionExpr) { @@ -1953,6 +2075,16 @@ public final class PyTypingTypeProvider extends PyTypeProviderWithCustomContext< } } } + if (expression instanceof PySubscriptionExpression subscriptionExpr) { + // Possibly a parameterized type alias + PyExpression operandExpression = subscriptionExpr.getOperand(); + List> results = tryResolvingWithAliases(operandExpression, context); + for (Pair pair : results) { + if (pair.getFirst() != null && pair.getSecond() != null) { + elements.add(Pair.create(pair.getFirst(), pair.getSecond())); + } + } + } return !elements.isEmpty() ? elements : Collections.singletonList(Pair.create(null, expression)); } 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 da07da231c3f..caaee3abac65 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 @@ -539,7 +539,15 @@ public final class PyCallExpressionHelper { } } } - if (callee instanceof PySubscriptionExpression) { + if (callee instanceof PySubscriptionExpression subscriptionExpression) { + PyExpression operandExpr = subscriptionExpression.getOperand(); + PyType operandType = context.getType(operandExpr); + if (operandType instanceof PyClassType classType) { + PyType parameterizeClassWithDefaults = PyTypingTypeProvider.tryParameterizeClassWithDefaults(classType, callee, true, context); + if (parameterizeClassWithDefaults instanceof PyCollectionType) { + return parameterizeClassWithDefaults; + } + } final PyType parametrizedType = Ref.deref(PyTypingTypeProvider.getType(callee, context)); if (parametrizedType != null) { return parametrizedType; @@ -650,9 +658,19 @@ public final class PyCallExpressionHelper { return new PyCollectionTypeImpl(receiverClass, false, elementTypes); } + if (initOrNewCallType instanceof PyClassType classType) { + PyType implicitlyParameterized = PyTypingTypeProvider.tryParameterizeClassWithDefaults(classType, callSite, true, context); + if (implicitlyParameterized instanceof PyCollectionType collectionType) { + return collectionType.toInstance(); + } + } + return new PyClassTypeImpl(receiverClass, false); } + if (initOrNewCallType instanceof PyCollectionType) { + return initOrNewCallType; + } if (initOrNewCallType == null) { return PyUnionType.createWeakType(new PyClassTypeImpl(receiverClass, false)); } diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyFunctionImpl.java b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyFunctionImpl.java index 2c8766eb7757..7d35f53c36e4 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyFunctionImpl.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyFunctionImpl.java @@ -223,7 +223,8 @@ public class PyFunctionImpl extends PyBaseElementImpl implements } final var substitutionsWithUnresolvedReturnGenerics = PyTypeChecker.getSubstitutionsWithUnresolvedReturnGenerics(getParameters(context), type, substitutions, context); - type = PyTypeChecker.substitute(type, substitutionsWithUnresolvedReturnGenerics, context); + final var substitutionsWithDefaults = PyTypeChecker.getSubstitutionsWithDefaults(substitutionsWithUnresolvedReturnGenerics); + type = PyTypeChecker.substitute(type, substitutionsWithDefaults, context); } else { type = null; diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyReferenceExpressionImpl.java b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyReferenceExpressionImpl.java index f06a1af9f18f..696160e0f629 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyReferenceExpressionImpl.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyReferenceExpressionImpl.java @@ -18,6 +18,7 @@ import com.jetbrains.python.codeInsight.controlflow.PyTypeAssertionEvaluator; import com.jetbrains.python.codeInsight.controlflow.ReadWriteInstruction; import com.jetbrains.python.codeInsight.controlflow.ScopeOwner; import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil; +import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider; import com.jetbrains.python.psi.*; import com.jetbrains.python.psi.impl.references.PyImportReference; import com.jetbrains.python.psi.impl.references.PyQualifiedReference; @@ -359,10 +360,21 @@ public class PyReferenceExpressionImpl extends PyElementImpl implements PyRefere PyType qualifierType = context.getType(qualifier); boolean possiblyParameterizedQualifier = !(qualifierType instanceof PyModuleType || qualifierType instanceof PyImportedModuleType); if (possiblyParameterizedQualifier && PyTypeChecker.hasGenerics(type, context)) { - final var substitutions = - PyTypeChecker.unifyGenericCall(qualifier, Collections.emptyMap(), context); + if (qualifierType instanceof PyCollectionType collectionType && collectionType.isDefinition()) { + PyCollectionType genericDefinitionType = PyTypeChecker.findGenericDefinitionType(collectionType.getPyClass(), context); + if (genericDefinitionType != null && type != null) { + List typeParameterTypes = + ContainerUtil.filterIsInstance(genericDefinitionType.getElementTypes(), PyTypeParameterType.class); + PyType typeWithSubstitutions = PyTypeChecker.trySubstituteByDefaultsOnly(type, typeParameterTypes, true, context); + if (typeWithSubstitutions != null) { + return typeWithSubstitutions; + } + } + } + final var substitutions = PyTypeChecker.unifyGenericCall(qualifier, Collections.emptyMap(), context); if (substitutions != null) { - final PyType substituted = PyTypeChecker.substitute(type, substitutions, context); + var substitutionsWithDefaults = PyTypeChecker.getSubstitutionsWithDefaults(substitutions); + final PyType substituted = PyTypeChecker.substitute(type, substitutionsWithDefaults, context); if (substituted != null) { return substituted; } @@ -371,6 +383,13 @@ public class PyReferenceExpressionImpl extends PyElementImpl implements PyRefere } } + if (type instanceof PyClassType classType && !(type instanceof PyCollectionType)) { + PyType parameterizedType = PyTypingTypeProvider.tryParameterizeClassWithDefaults(classType, anchor, false, context); + if (parameterizedType instanceof PyCollectionType collectionType) { + return collectionType; + } + } + return type; } diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PySubscriptionExpressionImpl.java b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PySubscriptionExpressionImpl.java index fd24d647524f..559c3d39f99f 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PySubscriptionExpressionImpl.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PySubscriptionExpressionImpl.java @@ -18,15 +18,13 @@ package com.jetbrains.python.psi.impl; import com.intellij.lang.ASTNode; import com.intellij.psi.PsiPolyVariantReference; import com.intellij.psi.PsiReference; +import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider; import com.jetbrains.python.psi.PyElementVisitor; import com.jetbrains.python.psi.PyExpression; import com.jetbrains.python.psi.PySubscriptionExpression; import com.jetbrains.python.psi.impl.references.PyOperatorReference; import com.jetbrains.python.psi.resolve.PyResolveContext; -import com.jetbrains.python.psi.types.PyTupleType; -import com.jetbrains.python.psi.types.PyType; -import com.jetbrains.python.psi.types.PyTypedDictType; -import com.jetbrains.python.psi.types.TypeEvalContext; +import com.jetbrains.python.psi.types.*; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -61,6 +59,12 @@ public class PySubscriptionExpressionImpl extends PyElementImpl implements PySub .map(typedDictType::getElementType) .orElse(null); } + if (type instanceof PyClassType classType) { + PyType parameterizedType = PyTypingTypeProvider.tryParameterizeClassWithDefaults(classType, this, false, context); + if (parameterizedType instanceof PyCollectionType collectionType) { + return collectionType.toClass(); + } + } return PyCallExpressionHelper.getCallType(this, context, key); } 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 1a9c8c9c62b7..265332ba09ea 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 @@ -961,10 +961,17 @@ public final class PyTypeChecker { boolean canGetBoundFromArguments = typeParamsFromParameterTypes.paramSpecs.contains(paramSpecType); boolean isAlreadyBound = existingSubstitutions.paramSpecs.containsKey(paramSpecType); if (canGetBoundFromArguments && !isAlreadyBound) { - existingSubstitutions.paramSpecs.put(paramSpecType, new PyParamSpecType(paramSpecType.getName()) - .withParameters(List.of(PyCallableParameterImpl.positionalNonPsi("args", null), - PyCallableParameterImpl.keywordNonPsi("kwargs", null)), - context)); + if (paramSpecType.getDefaultType() != null) { + PyType defaultType = paramSpecType.getDefaultType(); + if (defaultType instanceof PyParamSpecType defaultParamSpec) { + existingSubstitutions.paramSpecs.put(paramSpecType, defaultParamSpec); + } + } else { + existingSubstitutions.paramSpecs.put(paramSpecType, new PyParamSpecType(paramSpecType.getName()) + .withParameters(List.of(PyCallableParameterImpl.positionalNonPsi("args", null), + PyCallableParameterImpl.keywordNonPsi("kwargs", null)), + context)); + } } } return existingSubstitutions; @@ -1042,6 +1049,12 @@ public final class PyTypeChecker { } collectGenerics(callable.getReturnType(context), context, generics, visited); } + else if (type instanceof PyConcatenateType concatenateType) { + for (PyType elementType : concatenateType.getFirstTypes()) { + collectGenerics(elementType, context, generics, visited); + } + collectGenerics(concatenateType.getParamSpec(), context, generics, visited); + } else if (type instanceof PyUnpackedTupleType unpackedTupleType) { for (PyType elementType : unpackedTupleType.getElementTypes()) { collectGenerics(elementType, context, generics, visited); @@ -1108,6 +1121,17 @@ public final class PyTypeChecker { // // where we want to prevent replacing T with Any, even though it cannot be inferred at a call site. if (!substitutions.typeVars.containsKey(typeVar) && !substitutions.typeVars.containsKey(invert(typeVar))) { + PyTypeVarType substitution = StreamEx.of(substitutions.typeVars.keySet()) + .findFirst(typeVarType -> { + return typeVarType.getDeclarationElement() != null + && (typeVar.getScopeOwner() == null || (typeVarType.getScopeOwner() == typeVar.getScopeOwner())) + && typeVarType.getDeclarationElement().equals(typeVar.getDeclarationElement()); + }) + .orElse(null); + + if (substitution != null) { + return substitute(substitution, substitutions, context, substituting); + } return typeVar; } PyType substitution = substitutions.typeVars.get(typeVar); @@ -1118,6 +1142,26 @@ public final class PyTypeChecker { substitution = invert(invertedSubstitution); } } + if (substitution instanceof PyTypeVarType) { + PyQualifiedNameOwner scope = typeVar.getScopeOwner(); + if (scope instanceof PyClass pyClass) { + PyType genericTypeFromClass = getGenericTypeForClass(context, pyClass); + if (genericTypeFromClass instanceof PyCollectionType) { + Generics generics = collectGenerics(genericTypeFromClass, context); + PyType finalSubstitution = substitution; + + PyTypeVarType sameScopeSubstitution = StreamEx.of(generics.typeVars) + .findFirst(typeVarType -> { + return typeVarType.getDeclarationElement() != null + && typeVarType.getDeclarationElement().equals(finalSubstitution.getDeclarationElement()); + }).orElse(null); + + if (sameScopeSubstitution != null && sameScopeSubstitution.getDefaultType() != null) { + return substitute(sameScopeSubstitution, substitutions, context, substituting); + } + } + } + } // TODO remove !typeVar.equals(substitution) part, it's necessary due to the logic in unifyReceiverWithParamSpecs if (!typeVar.equals(substitution) && hasGenerics(substitution, context)) { return substitute(substitution, substitutions, context, substituting); @@ -1347,6 +1391,41 @@ public final class PyTypeChecker { return match(expectedType, actualType, context, substitutions); } + @NotNull + public static GenericSubstitutions getSubstitutionsWithDefaults(@NotNull GenericSubstitutions substitutions) { + Map typeVersWithDefaults = new HashMap<>(); + Map paramSpecsWithDefaults = new HashMap<>(); + Map typeVarTuplesWithDefaults = new HashMap<>(); + substitutions.getTypeVars().forEach((key, value) -> { + if (key.getDefaultType() != null && value == null) { + typeVersWithDefaults.put(key, key.getDefaultType()); + } + else if (value instanceof PyTypeVarType typeVarType + && typeVarType.getDefaultType() != null + && !typeVarType.getDefaultType().equals(key)) { + typeVersWithDefaults.put(key, typeVarType.getDefaultType()); + } + }); + substitutions.getParamSpecs().forEach((key, value) -> { + if (key.getDefaultType() instanceof PyParamSpecType defaultType + && (value == null || key.equals(value))) { + paramSpecsWithDefaults.put(key, defaultType); + } + }); + substitutions.getTypeVarTuples().forEach((key, value) -> { + if (key.getDefaultType() instanceof PyVariadicType variadicType + && (value == null + || (value instanceof PyUnpackedTupleType unpackedTupleType && unpackedTupleType.getElementTypes().isEmpty()) + || key.equals(value))) { + typeVarTuplesWithDefaults.put(key, variadicType); + } + }); + substitutions.typeVars.putAll(typeVersWithDefaults); + substitutions.paramSpecs.putAll(paramSpecsWithDefaults); + substitutions.typeVarTuples.putAll(typeVarTuplesWithDefaults); + return substitutions; + } + private static boolean matchContainer(@Nullable PyCallableParameter container, @NotNull List arguments, @NotNull GenericSubstitutions substitutions, @NotNull TypeEvalContext context) { if (container == null) { @@ -1557,21 +1636,13 @@ public final class PyTypeChecker { @NotNull TypeEvalContext context) { Generics typeParams = collectGenerics(genericType, context); if (!typeParams.isEmpty()) { - var substitutions = new GenericSubstitutions(); List expectedTypeParams = new ArrayList<>(new LinkedHashSet<>(typeParams.getAllTypeParameters())); - PyTypeParameterMapping mapping = PyTypeParameterMapping.mapByShape(expectedTypeParams, actualTypeParams, Option.MAP_UNMATCHED_EXPECTED_TYPES_TO_ANY); - if (mapping != null) { - for (Couple pair : mapping.getMappedTypes()) { - if (pair.getFirst() instanceof PyTypeVarType typeVar) { - substitutions.typeVars.put(typeVar, pair.getSecond()); - } - else if (pair.getFirst() instanceof PyTypeVarTupleType typeVarTuple) { - assert pair.getSecond() instanceof PyVariadicType; - substitutions.typeVarTuples.put(typeVarTuple, (PyVariadicType)pair.getSecond()); - } - } - } - return substitute(genericType, substitutions, context); + var substitutions = mapTypeParametersToSubstitutions(new GenericSubstitutions(), + expectedTypeParams, actualTypeParams, + Option.MAP_UNMATCHED_EXPECTED_TYPES_TO_ANY); + if (substitutions == null) return null; + var substitutionsWithDefaults = getSubstitutionsWithDefaults(substitutions); + return substitute(genericType, substitutionsWithDefaults, context); } // An already parameterized type, don't override existing values for type parameters else if (genericType instanceof PyCollectionType) { @@ -1581,9 +1652,54 @@ public final class PyTypeChecker { PyClass cls = ((PyClassType)genericType).getPyClass(); return new PyCollectionTypeImpl(cls, false, actualTypeParams); } - else { - return null; + return null; + } + + @ApiStatus.Internal + @Nullable + public static GenericSubstitutions mapTypeParametersToSubstitutions(@NotNull GenericSubstitutions substitutions, + @NotNull List expectedTypes, + @NotNull List actualTypes, + Option @NotNull ... options) { + PyTypeParameterMapping mapping = PyTypeParameterMapping.mapByShape(expectedTypes, actualTypes, options); + if (mapping != null) { + return fillSubstitutionsWithTypeParameters(substitutions, mapping.getMappedTypes()); } + return null; + } + + @NotNull + @ApiStatus.Internal + public static GenericSubstitutions fillSubstitutionsWithTypeParameters(@NotNull GenericSubstitutions substitutions, @NotNull List> typeParameters) { + for (Couple pair : typeParameters) { + if (pair.getFirst() instanceof PyTypeVarType typeVar) { + substitutions.typeVars.put(typeVar, pair.getSecond()); + } + else if (pair.getFirst() instanceof PyTypeVarTupleType typeVarTuple) { + substitutions.typeVarTuples.put(typeVarTuple, as(pair.getSecond(), PyVariadicType.class)); + } + else if (pair.getFirst() instanceof PyParamSpecType paramSpec) { + substitutions.paramSpecs.put(paramSpec, as(pair.getSecond(), PyParamSpecType.class)); + } + } + return substitutions; + } + + + @ApiStatus.Internal + @Nullable + public static PyType trySubstituteByDefaultsOnly(@NotNull PyType targetType, + List typeParameterTypes, + boolean unmappedToAny, + @NotNull TypeEvalContext context) { + if (!typeParameterTypes.isEmpty()) { + List> typeParametersToSubstitutions = ContainerUtil.map(typeParameterTypes, typeParameterType + -> new Couple<>(typeParameterType, unmappedToAny ? null : typeParameterType)); + var substitutions = fillSubstitutionsWithTypeParameters(new GenericSubstitutions(), typeParametersToSubstitutions); + var subsWithDefaults = getSubstitutionsWithDefaults(substitutions); + return substitute(targetType, subsWithDefaults, context); + } + return null; } @ApiStatus.Internal