From 80b33756293a836036d5f6def8d9d13fdca531ed Mon Sep 17 00:00:00 2001 From: Semyon Proshev Date: Tue, 14 Nov 2017 17:46:18 +0300 Subject: [PATCH] Handle namedtuples unpacking (PY-4351) --- .../codeInsight/stdlib/PyNamedTupleType.java | 14 +++---- .../documentation/PyTypeModelBuilder.java | 22 +++-------- .../psi/impl/PyTargetExpressionImpl.java | 4 +- .../python/psi/types/PyTupleType.java | 23 ++++------- .../python/psi/types/PyTypeChecker.java | 21 ++++++++++ .../PyStringFormatInspection/UnionCallType.py | 2 +- .../com/jetbrains/python/PyTypeTest.java | 38 +++++++++++++++++++ 7 files changed, 82 insertions(+), 42 deletions(-) diff --git a/python/src/com/jetbrains/python/codeInsight/stdlib/PyNamedTupleType.java b/python/src/com/jetbrains/python/codeInsight/stdlib/PyNamedTupleType.java index 53f1d9a98d91..9cf3c630124f 100644 --- a/python/src/com/jetbrains/python/codeInsight/stdlib/PyNamedTupleType.java +++ b/python/src/com/jetbrains/python/codeInsight/stdlib/PyNamedTupleType.java @@ -19,7 +19,7 @@ import java.util.*; /** * @author yole */ -public class PyNamedTupleType extends PyClassTypeImpl implements PyCallableType { +public class PyNamedTupleType extends PyTupleType implements PyCallableType { @NotNull private final PsiElement myDeclaration; @@ -38,7 +38,11 @@ public class PyNamedTupleType extends PyClassTypeImpl implements PyCallableType @NotNull String name, @NotNull Map fields, @NotNull DefinitionLevel definitionLevel) { - super(tupleClass, definitionLevel != DefinitionLevel.INSTANCE); + super(tupleClass, + Collections.unmodifiableList(ContainerUtil.map(fields.values(), typeAndValue -> typeAndValue.getType())), + false, + definitionLevel != DefinitionLevel.INSTANCE); + myDeclaration = declaration; myFields = Collections.unmodifiableMap(fields); myName = name; @@ -127,10 +131,6 @@ public class PyNamedTupleType extends PyClassTypeImpl implements PyCallableType return result; } - public int getElementCount() { - return myFields.size(); - } - @NotNull public Map getFields() { return myFields; @@ -152,7 +152,7 @@ public class PyNamedTupleType extends PyClassTypeImpl implements PyCallableType @NotNull private Map takeFieldsTypesFromCallSiteIfNeeded(@NotNull TypeEvalContext context, @NotNull PyCallSiteExpression callSite) { - if (StreamEx.ofValues(myFields).allMatch(typeAndValue -> typeAndValue.getType() == null)) { + if (StreamEx.of(getElementTypes()).allMatch(Objects::isNull)) { final List arguments = callSite.getArguments(null); if (arguments.size() == myFields.size()) { diff --git a/python/src/com/jetbrains/python/documentation/PyTypeModelBuilder.java b/python/src/com/jetbrains/python/documentation/PyTypeModelBuilder.java index fab6f2775dd5..50132c086242 100644 --- a/python/src/com/jetbrains/python/documentation/PyTypeModelBuilder.java +++ b/python/src/com/jetbrains/python/documentation/PyTypeModelBuilder.java @@ -1,18 +1,4 @@ -/* - * Copyright 2000-2017 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +// Copyright 2000-2017 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.documentation; import com.google.common.collect.Collections2; @@ -21,6 +7,7 @@ import com.intellij.openapi.util.Ref; import com.intellij.psi.PsiElement; import com.intellij.util.containers.ContainerUtil; import com.jetbrains.python.PyNames; +import com.jetbrains.python.codeInsight.stdlib.PyNamedTupleType; import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider; import com.jetbrains.python.psi.impl.PyBuiltinCache; import com.jetbrains.python.psi.types.*; @@ -233,7 +220,10 @@ public class PyTypeModelBuilder { myVisited.put(type, null); //mark as evaluating TypeModel result = null; - if (type instanceof PyTupleType) { + if (type instanceof PyNamedTupleType) { + result = NamedType.nameOrAny(type); + } + else if (type instanceof PyTupleType) { final PyTupleType tupleType = (PyTupleType)type; final List elementTypes = tupleType.isHomogeneous() diff --git a/python/src/com/jetbrains/python/psi/impl/PyTargetExpressionImpl.java b/python/src/com/jetbrains/python/psi/impl/PyTargetExpressionImpl.java index 3af270ccb8a6..68d85f305e59 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyTargetExpressionImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyTargetExpressionImpl.java @@ -183,8 +183,8 @@ public class PyTargetExpressionImpl extends PyBaseElementImpl elementTypes, boolean homogeneous) { - super(tupleClass, false); + this(tupleClass, elementTypes, homogeneous, false); + } + + protected PyTupleType(@NotNull PyClass tupleClass, @NotNull List elementTypes, boolean homogeneous, boolean isDefinition) { + super(tupleClass, isDefinition); myElementTypes = elementTypes; myHomogeneous = homogeneous; } + @Override @NotNull public String getName() { if (myHomogeneous) { diff --git a/python/src/com/jetbrains/python/psi/types/PyTypeChecker.java b/python/src/com/jetbrains/python/psi/types/PyTypeChecker.java index e5272ecf5dd8..53d403ece596 100644 --- a/python/src/com/jetbrains/python/psi/types/PyTypeChecker.java +++ b/python/src/com/jetbrains/python/psi/types/PyTypeChecker.java @@ -7,6 +7,7 @@ import com.intellij.psi.PsiFile; import com.intellij.util.ArrayUtil; import com.intellij.util.containers.ContainerUtil; import com.jetbrains.python.PyNames; +import com.jetbrains.python.codeInsight.stdlib.PyNamedTupleType; import com.jetbrains.python.psi.*; import com.jetbrains.python.psi.impl.PyBuiltinCache; import com.jetbrains.python.psi.impl.PyTypeProvider; @@ -704,6 +705,26 @@ public class PyTypeChecker { return null; } + @Nullable + public static PyType getTargetTypeFromTupleAssignment(@NotNull PyTargetExpression target, + @NotNull PyTupleExpression parentTuple, + @NotNull PyType assignedType, + @NotNull TypeEvalContext context) { + if (assignedType instanceof PyTupleType) { + return getTargetTypeFromTupleAssignment(target, parentTuple, (PyTupleType)assignedType); + } + else if (assignedType instanceof PyClassLikeType) { + return StreamEx + .of(((PyClassLikeType)assignedType).getAncestorTypes(context)) + .select(PyNamedTupleType.class) + .findFirst() + .map(t -> getTargetTypeFromTupleAssignment(target, parentTuple, t)) + .orElse(null); + } + + return null; + } + @Nullable public static PyType getTargetTypeFromTupleAssignment(@NotNull PyTargetExpression target, @NotNull PyTupleExpression parentTuple, @NotNull PyTupleType assignedTupleType) { diff --git a/python/testData/inspections/PyStringFormatInspection/UnionCallType.py b/python/testData/inspections/PyStringFormatInspection/UnionCallType.py index d32b0729cf7b..f58d32745961 100644 --- a/python/testData/inspections/PyStringFormatInspection/UnionCallType.py +++ b/python/testData/inspections/PyStringFormatInspection/UnionCallType.py @@ -37,7 +37,7 @@ def list_tuple(cond): "%s %s" % named_tuple_func(True) "%s %s" % named_tuple_func(False) -"%s %s %s" % named_tuple_func(False) +"%s %s %s" % named_tuple_func(False) "%s" % primitive_types_func(True) "%s %s" % primitive_types_func(True) diff --git a/python/testSrc/com/jetbrains/python/PyTypeTest.java b/python/testSrc/com/jetbrains/python/PyTypeTest.java index a27a607d9b90..cd4d416fd01e 100644 --- a/python/testSrc/com/jetbrains/python/PyTypeTest.java +++ b/python/testSrc/com/jetbrains/python/PyTypeTest.java @@ -2217,6 +2217,44 @@ public class PyTypeTest extends PyTestCase { "expr = User(\"name\", 13).age"); } + // PY-4351 + public void testTypingNTInheritorUnpacking() { + doTest("int", + "from typing import NamedTuple\n" + + "class User(NamedTuple(\"User\", [(\"name\", str), (\"age\", int)])):\n" + + " pass\n" + + "y2, expr = User(\"name\", 13)"); + } + + // PY-4351 + public void testTypingNTTargetUnpacking() { + doTest("int", + "from typing import NamedTuple\n" + + "Point2 = NamedTuple('Point', [('x', int), ('y', str)])\n" + + "p2 = Point2(1, \"1\")\n" + + "expr, y2 = p2"); + } + + // PY-4351 + public void testCollectionsNTInheritorUnpacking() { + // Seems that this case won't be supported because + // it requires to update ancestor, not class itself, for every `User(...)` call + doTest("Any", + "from collections import namedtuple\n" + + "class User(namedtuple(\"User\", \"name ags\")):\n" + + " pass\n" + + "y1, expr = User(\"name\", 13)"); + } + + // PY-4351 + public void testCollectionsNTTargetUnpacking() { + doTest("int", + "from collections import namedtuple\n" + + "Point = namedtuple('Point', ['x', 'y'])\n" + + "p1 = Point(1, '1')\n" + + "expr, y1 = p1"); + } + // PY-18791 public void testCallOnProperty() { runWithLanguageLevel(