PY-76825 Conformance test failure: namedtuples_define_functional.py

Merge-request: IJ-MR-157423
Merged-by: Aleksandr Govenko <aleksandr.govenko@jetbrains.com>

GitOrigin-RevId: 43e0729c540507d0a280d46aee4ca086e572d527
This commit is contained in:
Aleksandr.Govenko
2025-03-13 20:55:38 +00:00
committed by intellij-monorepo-bot
parent e09f1b5935
commit fdd47a988b
7 changed files with 94 additions and 60 deletions

View File

@@ -227,7 +227,7 @@ public final class PyUtilCore {
return name == null ? 0 : name.startsWith("__") ? 2 : name.startsWith(PyNames.UNDERSCORE) ? 1 : 0;
}
public static @Nullable List<String> strListValue(PyAstExpression value) {
public static @Nullable List<@NotNull String> strListValue(PyAstExpression value) {
while (value instanceof PyAstParenthesizedExpression) {
value = ((PyAstParenthesizedExpression)value).getContainedExpression();
}

View File

@@ -587,6 +587,17 @@ public final @NonNls class PyNames {
TRY
);
// As per: https://docs.python.org/3/reference/lexical_analysis.html#keywords
public static final Set<String> PY3_KEYWORDS = Set.of(
FALSE, AWAIT, ELSE, IMPORT, PASS,
NONE, BREAK, EXCEPT, IN, RAISE,
TRUE, CLASS, FINALLY, IS, RETURN,
AND, CONTINUE, FOR, LAMBDA, TRY,
AS, DEF, FROM, NONLOCAL, WHILE,
ASSERT, DEL, GLOBAL, NOT, WITH,
ASYNC, ELIF, IF, OR, YIELD
);
public static final Set<String> BUILTIN_INTERFACES = Set.of(
CALLABLE, HASHABLE, ITERABLE, ITERATOR, SIZED, CONTAINER, SEQUENCE, MAPPING, ABC_COMPLEX, ABC_REAL, ABC_RATIONAL, ABC_INTEGRAL,
ABC_NUMBER

View File

@@ -200,7 +200,7 @@ class PyNamedTupleTypeProvider : PyTypeProviderBase() {
stub.name,
parseNamedTupleFields(targetOrCall, fields, context),
true,
fields.values.any { it.isPresent },
fields.values.any { it.type != null },
getDeclaration(targetOrCall))
}
@@ -289,21 +289,21 @@ class PyNamedTupleTypeProvider : PyTypeProviderBase() {
return fields.stream().collect(toNTFields)
}
private fun parseNamedTupleFields(anchor: PsiElement, fields: Map<String, Optional<String>>, context: TypeEvalContext): NTFields {
private fun parseNamedTupleFields(anchor: PsiElement, fields: LinkedHashMap<String, PyNamedTupleStub.FieldTypeAndHasDefault>, context: TypeEvalContext): NTFields {
val result = NTFields()
for ((name, type) in fields) {
result[name] = parseNamedTupleField(anchor, type.orElse(null), context)
for ((name, typeAndDefault) in fields) {
result[name] = parseNamedTupleField(anchor, typeAndDefault.type(), typeAndDefault.hasDefault(), context)
}
return result
}
private fun parseNamedTupleField(anchor: PsiElement,
type: String?,
hasDefault: Boolean,
context: TypeEvalContext): PyNamedTupleType.FieldTypeAndDefaultValue {
if (type == null) return PyNamedTupleType.FieldTypeAndDefaultValue(null, null)
val pyType = Ref.deref(PyTypingTypeProvider.getStringBasedType(type, anchor, context))
return PyNamedTupleType.FieldTypeAndDefaultValue(pyType, null)
val pyType = type?.let { Ref.deref(PyTypingTypeProvider.getStringBasedType(type, anchor, context)) }
val defaultValue = if (hasDefault) PyElementGenerator.getInstance(anchor.project).createEllipsis() else null
return PyNamedTupleType.FieldTypeAndDefaultValue(pyType, defaultValue)
}
private fun getDeclaration(referenceTarget: PsiElement): PyQualifiedNameOwner? {

View File

@@ -1119,7 +1119,7 @@ public final class PyUtil {
return null;
}
public static @Nullable List<String> strListValue(PyExpression value) {
public static @Nullable List<@NotNull String> strListValue(PyExpression value) {
return PyUtilCore.strListValue(value);
}

View File

@@ -21,7 +21,6 @@ import com.intellij.psi.PsiElement;
import com.intellij.psi.stubs.StubInputStream;
import com.intellij.psi.stubs.StubOutputStream;
import com.intellij.psi.util.QualifiedName;
import com.intellij.util.ArrayUtil;
import com.intellij.util.containers.ContainerUtil;
import com.jetbrains.python.PyNames;
import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider;
@@ -35,9 +34,9 @@ import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import static com.jetbrains.python.psi.PyUtil.as;
import static com.jetbrains.python.psi.impl.PyPsiUtils.flattenParens;
public final class PyNamedTupleStubImpl implements PyNamedTupleStub {
@@ -45,11 +44,11 @@ public final class PyNamedTupleStubImpl implements PyNamedTupleStub {
private final @NotNull String myName;
private final @NotNull LinkedHashMap<String, Optional<String>> myFields;
private final @NotNull LinkedHashMap<String, FieldTypeAndHasDefault> myFields;
private PyNamedTupleStubImpl(@Nullable QualifiedName calleeName,
@NotNull String name,
@NotNull LinkedHashMap<String, Optional<String>> fields) {
@NotNull LinkedHashMap<String, FieldTypeAndHasDefault> fields) {
myCalleeName = calleeName;
myName = name;
myFields = fields;
@@ -66,7 +65,7 @@ public final class PyNamedTupleStubImpl implements PyNamedTupleStub {
}
public static @Nullable PyNamedTupleStub create(@NotNull PyCallExpression expression) {
final PyReferenceExpression calleeReference = PyUtil.as(expression.getCallee(), PyReferenceExpression.class);
final PyReferenceExpression calleeReference = as(expression.getCallee(), PyReferenceExpression.class);
if (calleeReference == null) {
return null;
@@ -81,7 +80,7 @@ public final class PyNamedTupleStubImpl implements PyNamedTupleStub {
return null;
}
final LinkedHashMap<String, Optional<String>> fields = resolveTupleFields(expression, calleeNameAndModule.getSecond());
final LinkedHashMap<String, FieldTypeAndHasDefault> fields = resolveTupleFields(expression, calleeNameAndModule.getSecond());
if (fields == null) {
return null;
@@ -96,7 +95,7 @@ public final class PyNamedTupleStubImpl implements PyNamedTupleStub {
public static @Nullable PyNamedTupleStub deserialize(@NotNull StubInputStream stream) throws IOException {
final String calleeName = stream.readNameString();
final String name = stream.readNameString();
final LinkedHashMap<String, Optional<String>> fields = deserializeFields(stream, stream.readVarInt());
final LinkedHashMap<String, FieldTypeAndHasDefault> fields = deserializeFields(stream, stream.readVarInt());
if (calleeName == null || name == null) {
return null;
@@ -116,9 +115,10 @@ public final class PyNamedTupleStubImpl implements PyNamedTupleStub {
stream.writeName(myName);
stream.writeVarInt(myFields.size());
for (Map.Entry<String, Optional<String>> entry : myFields.entrySet()) {
for (var entry : myFields.entrySet()) {
stream.writeName(entry.getKey());
stream.writeName(entry.getValue().orElse(null));
stream.writeName(entry.getValue().type());
stream.writeBoolean(entry.getValue().hasDefault());
}
}
@@ -133,7 +133,7 @@ public final class PyNamedTupleStubImpl implements PyNamedTupleStub {
}
@Override
public @NotNull Map<String, Optional<String>> getFields() {
public @NotNull LinkedHashMap<String, FieldTypeAndHasDefault> getFields() {
return myFields;
}
@@ -153,31 +153,29 @@ public final class PyNamedTupleStubImpl implements PyNamedTupleStub {
return null;
}
private static @Nullable LinkedHashMap<String, Optional<String>> resolveTupleFields(@NotNull PyCallExpression callExpression,
@NotNull NamedTupleModule module) {
private static @Nullable LinkedHashMap<String, FieldTypeAndHasDefault> resolveTupleFields(@NotNull PyCallExpression callExpression, @NotNull NamedTupleModule module) {
return switch (module) {
case TYPING -> resolveTypingNTFields(callExpression);
case COLLECTIONS -> resolveCollectionsNTFields(callExpression);
};
}
private static @NotNull LinkedHashMap<String, Optional<String>> deserializeFields(@NotNull StubInputStream stream, int fieldsSize)
private static @NotNull LinkedHashMap<String, FieldTypeAndHasDefault> deserializeFields(@NotNull StubInputStream stream, int fieldsSize)
throws IOException {
final LinkedHashMap<String, Optional<String>> fields = new LinkedHashMap<>(fieldsSize);
final LinkedHashMap<String, FieldTypeAndHasDefault> fields = new LinkedHashMap<>(fieldsSize);
for (int i = 0; i < fieldsSize; i++) {
final String name = stream.readNameString();
final String type = stream.readNameString();
final boolean hasDefault = stream.readBoolean();
if (name != null) {
fields.put(name, Optional.ofNullable(type));
}
fields.put(name, new FieldTypeAndHasDefault(type, hasDefault));
}
return fields;
}
private static @Nullable LinkedHashMap<String, Optional<String>> resolveCollectionsNTFields(@NotNull PyCallExpression callExpression) {
private static @Nullable LinkedHashMap<String, FieldTypeAndHasDefault> resolveCollectionsNTFields(@NotNull PyCallExpression callExpression) {
// SUPPORTED CASES:
// fields = ["x", "y"]
@@ -199,21 +197,35 @@ public final class PyNamedTupleStubImpl implements PyNamedTupleStub {
? PyResolveUtil.fullResolveLocally((PyReferenceExpression)fields)
: fields;
final Collector<String, ?, LinkedHashMap<String, Optional<String>>> toFieldsOfUnknownType =
Collectors.toMap(Function.identity(), key -> Optional.empty(), (v1, v2) -> v2, LinkedHashMap::new);
List<@NotNull String> fieldNames = PyUtil.strListValue(resolvedFields);
if (fieldNames == null) {
final String resolvedFieldsValue = PyPsiUtils.strValue(resolvedFields);
if (resolvedFieldsValue == null) return null;
fieldNames = StreamEx
.of(StringUtil.tokenize(resolvedFieldsValue, ", ").iterator()).toList();
}
final List<String> listValue = PyUtil.strListValue(resolvedFields);
if (listValue != null) return listValue.contains(null) ? null : StreamEx.of(listValue).collect(toFieldsOfUnknownType);
final PyExpression defaults = getDefaultsArgumentValue(callExpression);
final PyExpression resolvedDefaults = defaults instanceof PyReferenceExpression
? PyResolveUtil.fullResolveLocally((PyReferenceExpression)defaults)
: defaults;
int defaultStart = fieldNames.size();
if (resolvedDefaults instanceof PySequenceExpression seq) {
defaultStart -= seq.getElements().length;
}
final String resolvedFieldsValue = PyPsiUtils.strValue(resolvedFields);
if (resolvedFieldsValue == null) return null;
LinkedHashMap<String, FieldTypeAndHasDefault> result = new LinkedHashMap<>(fieldNames.size());
for (int i = 0; i < fieldNames.size(); i++) {
if (PyNames.PY3_KEYWORDS.contains(fieldNames.get(i))) {
fieldNames.set(i, "_" + i);
}
result.put(fieldNames.get(i), new FieldTypeAndHasDefault(null, i >= defaultStart));
}
return StreamEx
.of(StringUtil.tokenize(resolvedFieldsValue, ", ").iterator())
.collect(toFieldsOfUnknownType);
return result;
}
private static @Nullable LinkedHashMap<String, Optional<String>> resolveTypingNTFields(@NotNull PyCallExpression callExpression) {
private static @Nullable LinkedHashMap<String, FieldTypeAndHasDefault> resolveTypingNTFields(@NotNull PyCallExpression callExpression) {
// SUPPORTED CASES:
// fields = [("x", str), ("y", int)]
@@ -242,11 +254,15 @@ public final class PyNamedTupleStubImpl implements PyNamedTupleStub {
}
private static @Nullable PyExpression getFieldsArgumentValue(@NotNull PyCallExpression callExpression, @NotNull String possibleKeyword) {
return PyPsiUtils.flattenParens(callExpression.getArgument(1, possibleKeyword, PyExpression.class));
return flattenParens(callExpression.getArgument(1, possibleKeyword, PyExpression.class));
}
private static @Nullable LinkedHashMap<String, Optional<String>> getTypingNTFieldsFromKwArguments(@NotNull List<PyExpression> arguments) {
final LinkedHashMap<String, Optional<String>> result = new LinkedHashMap<>();
private static @Nullable PyExpression getDefaultsArgumentValue(@NotNull PyCallExpression callExpression) {
return flattenParens(as(callExpression.getKeywordArgument("defaults"), PyExpression.class));
}
private static @Nullable LinkedHashMap<String, FieldTypeAndHasDefault> getTypingNTFieldsFromKwArguments(@NotNull List<PyExpression> arguments) {
final LinkedHashMap<String, FieldTypeAndHasDefault> result = new LinkedHashMap<>(arguments.size());
for (PyExpression argument : arguments) {
if (!(argument instanceof PyKeywordArgument keywordArgument)) return null;
@@ -254,14 +270,14 @@ public final class PyNamedTupleStubImpl implements PyNamedTupleStub {
final String keyword = keywordArgument.getKeyword();
if (keyword == null) return null;
result.put(keyword, Optional.ofNullable(textIfPresent(keywordArgument.getValueExpression())));
result.put(keyword, new FieldTypeAndHasDefault(textIfPresent(keywordArgument.getValueExpression()), false));
}
return result;
}
private static @Nullable LinkedHashMap<String, Optional<String>> getTypingNTFieldsFromIterable(@NotNull PySequenceExpression fields) {
final LinkedHashMap<String, Optional<String>> result = new LinkedHashMap<>();
private static @Nullable LinkedHashMap<String, FieldTypeAndHasDefault> getTypingNTFieldsFromIterable(@NotNull PySequenceExpression fields) {
final LinkedHashMap<String, FieldTypeAndHasDefault> result = new LinkedHashMap<>(fields.getElements().length);
for (PyExpression element : fields.getElements()) {
if (!(element instanceof PyParenthesizedExpression)) return null;
@@ -270,19 +286,12 @@ public final class PyNamedTupleStubImpl implements PyNamedTupleStub {
if (!(contained instanceof PyTupleExpression)) return null;
final PyExpression[] nameAndType = ((PyTupleExpression)contained).getElements();
final PyExpression name = ArrayUtil.getFirstElement(nameAndType);
if (nameAndType.length != 2) return null;
String nameValue = null;
if (name instanceof PyStringLiteralExpression) {
nameValue = ((PyStringLiteralExpression)name).getStringValue();
}
else if (name instanceof PyReferenceExpression) {
nameValue = PyPsiUtils.strValue(PyResolveUtil.fullResolveLocally((PyReferenceExpression)name));
}
if (nameValue == null) return null;
final String name = tryResolveToText(nameAndType[0]);
if (name == null) return null;
result.put(nameValue, Optional.ofNullable(textIfPresent(nameAndType[1])));
result.put(name, new FieldTypeAndHasDefault(textIfPresent(nameAndType[1]), false));
}
return result;
@@ -292,6 +301,19 @@ public final class PyNamedTupleStubImpl implements PyNamedTupleStub {
return element == null ? null : element.getText();
}
private static @Nullable String tryResolveToText(@Nullable PyExpression expression) {
if (expression instanceof PyStringLiteralExpression) {
return ((PyStringLiteralExpression)expression).getStringValue();
}
else if (expression instanceof PyReferenceExpression) {
final PyExpression resolved = PyResolveUtil.fullResolveLocally((PyReferenceExpression)expression);
if (resolved instanceof PyStringLiteralExpression) {
return ((PyStringLiteralExpression)resolved).getStringValue();
}
}
return null;
}
@Override
public String toString() {
return "PyNamedTupleStub(calleeName=" + myCalleeName + ", name=" + myName + ", fields=" + myFields + ')';

View File

@@ -17,9 +17,9 @@ package com.jetbrains.python.psi.stubs;
import com.jetbrains.python.psi.impl.stubs.CustomTargetExpressionStub;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Map;
import java.util.Optional;
import java.util.LinkedHashMap;
public interface PyNamedTupleStub extends CustomTargetExpressionStub {
@@ -34,5 +34,7 @@ public interface PyNamedTupleStub extends CustomTargetExpressionStub {
* Iteration order repeats the declaration order.
*/
@NotNull
Map<String, Optional<String>> getFields();
LinkedHashMap<String, FieldTypeAndHasDefault> getFields();
record FieldTypeAndHasDefault(@Nullable String type, boolean hasDefault) {}
}

View File

@@ -54,7 +54,6 @@ historical_positional.py
literals_literalstring.py
literals_semantics.py
namedtuples_define_class.py
namedtuples_define_functional.py
namedtuples_type_compat.py
namedtuples_usage.py
narrowing_typeis.py