diff --git a/python/src/com/jetbrains/python/codeInsight/stdlib/PyNamedTupleType.java b/python/src/com/jetbrains/python/codeInsight/stdlib/PyNamedTupleType.java index 025084cd4b1c..519b9ac5b4d5 100644 --- a/python/src/com/jetbrains/python/codeInsight/stdlib/PyNamedTupleType.java +++ b/python/src/com/jetbrains/python/codeInsight/stdlib/PyNamedTupleType.java @@ -45,7 +45,7 @@ public class PyNamedTupleType extends PyClassTypeImpl implements PyCallableType private final String myName; @NotNull - private final Map> myFields; + private final Map myFields; @NotNull private final DefinitionLevel myDefinitionLevel; @@ -53,7 +53,7 @@ public class PyNamedTupleType extends PyClassTypeImpl implements PyCallableType public PyNamedTupleType(@NotNull PyClass tupleClass, @NotNull PsiElement declaration, @NotNull String name, - @NotNull Map> fields, + @NotNull Map fields, @NotNull DefinitionLevel definitionLevel) { super(tupleClass, definitionLevel != DefinitionLevel.INSTANCE); myDeclaration = declaration; @@ -149,7 +149,7 @@ public class PyNamedTupleType extends PyClassTypeImpl implements PyCallableType } @NotNull - public Map> getFields() { + public Map getFields() { return myFields; } @@ -162,14 +162,43 @@ public class PyNamedTupleType extends PyClassTypeImpl implements PyCallableType @Override public List getParameters(@NotNull TypeEvalContext context) { return isCallable() - ? ContainerUtil.map(myFields.entrySet(), field -> new PyCallableParameterImpl(field.getKey(), field.getValue().orElse(null))) + ? ContainerUtil.map(myFields.entrySet(), field -> fieldToCallableParameter(field.getKey(), field.getValue())) : null; } + @NotNull + private static PyCallableParameter fieldToCallableParameter(@NotNull String name, @NotNull FieldTypeAndDefaultValue typeAndDefaultValue) { + return new PyCallableParameterImpl(name, typeAndDefaultValue.getType(), typeAndDefaultValue.getDefaultValue()); + } + public enum DefinitionLevel { AS_SUPERCLASS, NEW_TYPE, INSTANCE } + + public static class FieldTypeAndDefaultValue { + + @Nullable + private final PyType myType; + + @Nullable + private final PyExpression myDefaultValue; + + public FieldTypeAndDefaultValue(@Nullable PyType type, @Nullable PyExpression defaultValue) { + myType = type; + myDefaultValue = defaultValue; + } + + @Nullable + public PyType getType() { + return myType; + } + + @Nullable + public PyExpression getDefaultValue() { + return myDefaultValue; + } + } } diff --git a/python/src/com/jetbrains/python/codeInsight/stdlib/PyStdlibTypeProvider.java b/python/src/com/jetbrains/python/codeInsight/stdlib/PyStdlibTypeProvider.java index c3fdb5997014..9bd43c2978cb 100644 --- a/python/src/com/jetbrains/python/codeInsight/stdlib/PyStdlibTypeProvider.java +++ b/python/src/com/jetbrains/python/codeInsight/stdlib/PyStdlibTypeProvider.java @@ -404,7 +404,7 @@ public class PyStdlibTypeProvider extends PyTypeProviderBase { return new PyNamedTupleType(tupleClass, referenceTarget, stub.getName(), - parseNamedTupleFieldsTypes(referenceTarget, stub.getFields(), context), + parseNamedTupleFields(referenceTarget, stub.getFields(), context), definitionLevel); } @@ -431,27 +431,33 @@ public class PyStdlibTypeProvider extends PyTypeProviderBase { } @NotNull - private static LinkedHashMap> parseNamedTupleFieldsTypes(@NotNull PsiElement anchor, - @NotNull Map> fields, - @NotNull TypeEvalContext context) { - final LinkedHashMap> result = new LinkedHashMap<>(); + private static LinkedHashMap parseNamedTupleFields(@NotNull PsiElement anchor, + @NotNull Map> fields, + @NotNull TypeEvalContext context) { + final LinkedHashMap result = new LinkedHashMap<>(); for (Map.Entry> entry : fields.entrySet()) { - result.put(entry.getKey(), entry.getValue().map(type -> parseNamedTupleFieldType(anchor, type, context))); + result.put(entry.getKey(), parseNamedTupleField(anchor, entry.getValue().orElse(null), context)); } return result; } @Nullable - private static PyType parseNamedTupleFieldType(@NotNull PsiElement anchor, @NotNull String type, @NotNull TypeEvalContext context) { + private static PyNamedTupleType.FieldTypeAndDefaultValue parseNamedTupleField(@NotNull PsiElement anchor, + @Nullable String type, + @NotNull TypeEvalContext context) { + if (type == null) return new PyNamedTupleType.FieldTypeAndDefaultValue(null, null); + final PyExpressionCodeFragmentImpl codeFragment = new PyExpressionCodeFragmentImpl(anchor.getProject(), "dummy.py", type, false); codeFragment.setContext(anchor.getContainingFile()); final PsiElement element = codeFragment.getFirstChild(); if (element instanceof PyExpressionStatement) { final PyExpression expression = ((PyExpressionStatement)element).getExpression(); - return Ref.deref(PyTypingTypeProvider.getType(expression, context)); + final PyType pyType = Ref.deref(PyTypingTypeProvider.getType(expression, context)); + + return new PyNamedTupleType.FieldTypeAndDefaultValue(pyType, null); } return null; diff --git a/python/src/com/jetbrains/python/psi/impl/PyClassImpl.java b/python/src/com/jetbrains/python/psi/impl/PyClassImpl.java index ff423c06be4e..e3ca716e2000 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyClassImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyClassImpl.java @@ -172,16 +172,16 @@ public class PyClassImpl extends PyBaseElementImpl implements PyCla } ); - final Collector>> toTypedFields = + final Collector> toNTFields = Collectors.toMap(PyTargetExpression::getName, - field -> Optional.ofNullable(context.getType(field)), + field -> new PyNamedTupleType.FieldTypeAndDefaultValue(context.getType(field), field.findAssignedValue()), (v1, v2) -> v2, LinkedHashMap::new); return new PyNamedTupleType(tupleClass, this, name, - fields.stream().collect(toTypedFields), + fields.stream().collect(toNTFields), PyNamedTupleType.DefinitionLevel.NEW_TYPE); } } diff --git a/python/src/com/jetbrains/python/psi/types/PyCallableParameterImpl.java b/python/src/com/jetbrains/python/psi/types/PyCallableParameterImpl.java index 73457f17eefe..60affc69af8e 100644 --- a/python/src/com/jetbrains/python/psi/types/PyCallableParameterImpl.java +++ b/python/src/com/jetbrains/python/psi/types/PyCallableParameterImpl.java @@ -32,17 +32,24 @@ import java.util.Objects; public class PyCallableParameterImpl implements PyCallableParameter { @Nullable private final String myName; @Nullable private final PyType myType; + @Nullable private final PyExpression myDefaultValue; @Nullable private final PyParameter myElement; public PyCallableParameterImpl(@Nullable String name, @Nullable PyType type) { + this(name, type, null); + } + + public PyCallableParameterImpl(@Nullable String name, @Nullable PyType type, @Nullable PyExpression defaultValue) { myName = name; myType = type; + myDefaultValue = defaultValue; myElement = null; } public PyCallableParameterImpl(@NotNull PyParameter element) { myName = null; myType = null; + myDefaultValue = null; myElement = element; } @@ -79,12 +86,12 @@ public class PyCallableParameterImpl implements PyCallableParameter { @Nullable @Override public PyExpression getDefaultValue() { - return myElement == null ? null : myElement.getDefaultValue(); + return myElement == null ? myDefaultValue : myElement.getDefaultValue(); } @Override public boolean hasDefaultValue() { - return myElement != null && myElement.hasDefaultValue(); + return myElement == null ? myDefaultValue != null : myElement.hasDefaultValue(); } @Override diff --git a/python/testData/inspections/PyArgumentListInspection/initializingTypingNamedTupleWithDefaultValues.py b/python/testData/inspections/PyArgumentListInspection/initializingTypingNamedTupleWithDefaultValues.py new file mode 100644 index 000000000000..561d1cab9c9d --- /dev/null +++ b/python/testData/inspections/PyArgumentListInspection/initializingTypingNamedTupleWithDefaultValues.py @@ -0,0 +1,24 @@ +import typing + + +class MyTup5(typing.NamedTuple): + bar: int + baz: str = "" + + +# empty +MyTup5() + +# one +MyTup5('') +MyTup5(bar='') +MyTup5(baz='') + +# two +MyTup5('', '') +MyTup5(bar='', baz='') +MyTup5(baz='', bar='') + +# three +MyTup5(bar='', baz='', foo='') +MyTup5('', '', '') diff --git a/python/testData/paramInfo/InitializingTypingNamedTuple.py b/python/testData/paramInfo/InitializingTypingNamedTuple.py index d8ae3bd4b8c2..25fcb257657b 100644 --- a/python/testData/paramInfo/InitializingTypingNamedTuple.py +++ b/python/testData/paramInfo/InitializingTypingNamedTuple.py @@ -26,9 +26,15 @@ class MyTup6(typing.NamedTuple): MyTup7 = typing.NamedTuple("MyTup7", names=List[str], ages=List[int]) +class MyTup8(typing.NamedTuple): + bar: int + baz: str = "" + + MyTup2() MyTup3() MyTup4() MyTup5() MyTup6() MyTup7() +MyTup8() diff --git a/python/testSrc/com/jetbrains/python/PyParameterInfoTest.java b/python/testSrc/com/jetbrains/python/PyParameterInfoTest.java index c07c837576ba..b16b560c0411 100644 --- a/python/testSrc/com/jetbrains/python/PyParameterInfoTest.java +++ b/python/testSrc/com/jetbrains/python/PyParameterInfoTest.java @@ -672,7 +672,7 @@ public class PyParameterInfoTest extends LightMarkedTestCase { runWithLanguageLevel( LanguageLevel.PYTHON35, () -> { - final Map test = loadTest(6); + final Map test = loadTest(7); for (int offset : StreamEx.of(1, 2, 3, 4).map(number -> test.get("").getTextOffset())) { final List texts = Collections.singletonList("bar: int, baz: str"); @@ -688,6 +688,10 @@ public class PyParameterInfoTest extends LightMarkedTestCase { final List texts2 = Collections.singletonList("names: List[str], ages: List[int]"); final List highlighted2 = Collections.singletonList(new String[]{"names: List[str], "}); feignCtrlP(test.get("").getTextOffset()).check(texts2, highlighted2, Collections.singletonList(ArrayUtil.EMPTY_STRING_ARRAY)); + + final List texts3 = Collections.singletonList("bar: int, baz: str=\"\""); + final List highlighted3 = Collections.singletonList(new String[]{"bar: int, "}); + feignCtrlP(test.get("").getTextOffset()).check(texts3, highlighted3, Collections.singletonList(ArrayUtil.EMPTY_STRING_ARRAY)); } ); } diff --git a/python/testSrc/com/jetbrains/python/PyStubsTest.java b/python/testSrc/com/jetbrains/python/PyStubsTest.java index c5a89239ee36..42b8740e1d85 100644 --- a/python/testSrc/com/jetbrains/python/PyStubsTest.java +++ b/python/testSrc/com/jetbrains/python/PyStubsTest.java @@ -648,12 +648,18 @@ public class PyStubsTest extends PyTestCase { final Iterator fieldsNamesIterator = expectedFieldsNames.iterator(); final Iterator fieldsTypesIterator = expectedFieldsTypes.iterator(); - for (Map.Entry> entry : namedTupleType.getFields().entrySet()) { + for (Map.Entry entry : namedTupleType.getFields().entrySet()) { assertTrue(fieldsNamesIterator.hasNext()); assertTrue(fieldsTypesIterator.hasNext()); - assertEquals(fieldsNamesIterator.next(), entry.getKey()); - assertEquals(fieldsTypesIterator.next(), entry.getValue().map(PyType::getName).orElse(null)); + final String fieldName = entry.getKey(); + final PyNamedTupleType.FieldTypeAndDefaultValue fieldTypeAndDefaultValue = entry.getValue(); + + assertEquals(fieldsNamesIterator.next(), fieldName); + + final PyType fieldType = fieldTypeAndDefaultValue.getType(); + assertEquals(fieldsTypesIterator.next(), fieldType == null ? null : fieldType.getName()); + assertNull(fieldTypeAndDefaultValue.getDefaultValue()); } assertFalse(fieldsNamesIterator.hasNext()); diff --git a/python/testSrc/com/jetbrains/python/inspections/PyArgumentListInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyArgumentListInspectionTest.java index 1d2fe98221fb..fa786b2cb2c9 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyArgumentListInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyArgumentListInspectionTest.java @@ -278,6 +278,11 @@ public class PyArgumentListInspectionTest extends PyTestCase { runWithLanguageLevel(LanguageLevel.PYTHON36, this::doTest); } + // PY-24099 + public void testInitializingTypingNamedTupleWithDefaultValues() { + runWithLanguageLevel(LanguageLevel.PYTHON36, this::doTest); + } + // PY-4344, PY-8422, PY-22269, PY-22740 public void testInitializingCollectionsNamedTuple() { doTest();