From 49b3258737ded61d2d37bfcd725d4525c4f788ed Mon Sep 17 00:00:00 2001 From: Petr Date: Thu, 19 Dec 2024 13:02:03 +0100 Subject: [PATCH] PY-76812 Conformance test failure: typeddicts_alt_syntax.py Infer typed dict type from TypedDict() call even in case of an unexpected arguments list. Also changed incorrect TypedDict() signature: from (name: str, fields: dict[str, Any], total: bool = True) -> Any to (typename: str, fields: dict[str, type], *, /, total: bool = True) -> Any GitOrigin-RevId: 94c1ebb4fe3026744cb9771eab9e5ba507d049fa --- .../python/psi/PyElementGenerator.java | 3 ++ .../typing/PyTypedDictTypeProvider.kt | 27 ++++++----- .../inspections/PyTypedDictInspection.kt | 34 +++++++------- .../psi/impl/PyElementGeneratorImpl.java | 5 ++ .../psi/impl/stubs/PyTypedDictStubImpl.kt | 46 +++++++------------ .../python/psi/resolve/PyResolveUtil.java | 8 +++- .../python/psi/types/PyTypedDictType.kt | 4 +- .../typedDictAlternativeSyntaxDefinition.py | 7 ++- .../testData/stubs/TypedDictFieldsKeyword.py | 3 -- python/testData/stubs/TypedDictNameKeyword.py | 3 -- .../jetbrains/python/PyParameterInfoTest.java | 4 +- .../com/jetbrains/python/PyStubsTest.java | 10 ---- .../PyTypeCheckerInspectionTest.java | 10 +++- 13 files changed, 79 insertions(+), 85 deletions(-) delete mode 100644 python/testData/stubs/TypedDictFieldsKeyword.py delete mode 100644 python/testData/stubs/TypedDictNameKeyword.py diff --git a/python/python-psi-api/src/com/jetbrains/python/psi/PyElementGenerator.java b/python/python-psi-api/src/com/jetbrains/python/psi/PyElementGenerator.java index 6cd81e85a7f6..ea068a29d1d4 100644 --- a/python/python-psi-api/src/com/jetbrains/python/psi/PyElementGenerator.java +++ b/python/python-psi-api/src/com/jetbrains/python/psi/PyElementGenerator.java @@ -157,4 +157,7 @@ public abstract class PyElementGenerator extends PyAstElementGenerator { @NotNull public abstract PySingleStarParameter createSingleStarParameter(); + + @NotNull + public abstract PySlashParameter createSlashParameter(); } diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypedDictTypeProvider.kt b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypedDictTypeProvider.kt index 475c6371b060..e15c66bde66f 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypedDictTypeProvider.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypedDictTypeProvider.kt @@ -16,8 +16,6 @@ import com.jetbrains.python.psi.impl.stubs.PyTypedDictStubImpl import com.jetbrains.python.psi.stubs.PyTypedDictFieldStub import com.jetbrains.python.psi.stubs.PyTypedDictStub import com.jetbrains.python.psi.types.* -import com.jetbrains.python.psi.types.PyTypedDictType.Companion.TYPED_DICT_FIELDS_PARAMETER -import com.jetbrains.python.psi.types.PyTypedDictType.Companion.TYPED_DICT_NAME_PARAMETER import com.jetbrains.python.psi.types.PyTypedDictType.Companion.TYPED_DICT_TOTAL_PARAMETER import java.util.* import java.util.stream.Collectors @@ -116,22 +114,27 @@ class PyTypedDictTypeProvider : PyTypeProviderBase() { if (PyCallExpressionNavigator.getPyCallExpressionByCallee(referenceExpression) == null) return null if (isTypedDict(referenceExpression, context)) { - val parameters = mutableListOf() - val builtinCache = PyBuiltinCache.getInstance(referenceExpression) val languageLevel = LanguageLevel.forElement(referenceExpression) val generator = PyElementGenerator.getInstance(referenceExpression.project) - parameters.add(PyCallableParameterImpl.nonPsi(TYPED_DICT_NAME_PARAMETER, builtinCache.getStringType(languageLevel))) - val dictClassType = builtinCache.dictType - parameters.add(PyCallableParameterImpl.nonPsi(TYPED_DICT_FIELDS_PARAMETER, - if (dictClassType != null) PyCollectionTypeImpl(dictClassType.pyClass, false, - listOf(builtinCache.strType, null)) - else null)) - parameters.add( + val dictType = builtinCache.dictType + val strToTypeDictType = if (dictType != null) { + PyCollectionTypeImpl(dictType.pyClass, false, listOf(builtinCache.strType, builtinCache.typeType)) + } + else { + null + } + + val parameters = listOf( + PyCallableParameterImpl.nonPsi("typename", builtinCache.getStringType(languageLevel)), + PyCallableParameterImpl.nonPsi("fields", strToTypeDictType), + PyCallableParameterImpl.psi(generator.createSingleStarParameter()), + PyCallableParameterImpl.psi(generator.createSlashParameter()), PyCallableParameterImpl.nonPsi(TYPED_DICT_TOTAL_PARAMETER, builtinCache.boolType, - generator.createExpressionFromText(languageLevel, PyNames.TRUE))) + generator.createExpressionFromText(languageLevel, PyNames.TRUE)) + ) return PyCallableTypeImpl(parameters, null) } diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypedDictInspection.kt b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypedDictInspection.kt index 33fbfd775e8c..a59025f1695b 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypedDictInspection.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypedDictInspection.kt @@ -18,8 +18,6 @@ import com.jetbrains.python.psi.* import com.jetbrains.python.psi.impl.PyEvaluator import com.jetbrains.python.psi.impl.PyPsiUtils import com.jetbrains.python.psi.types.* -import com.jetbrains.python.psi.types.PyTypedDictType.Companion.TYPED_DICT_FIELDS_PARAMETER -import com.jetbrains.python.psi.types.PyTypedDictType.Companion.TYPED_DICT_NAME_PARAMETER import com.jetbrains.python.psi.types.PyTypedDictType.Companion.TYPED_DICT_TOTAL_PARAMETER class PyTypedDictInspection : PyInspection() { @@ -60,7 +58,7 @@ class PyTypedDictInspection : PyInspection() { override fun visitPyTargetExpression(node: PyTargetExpression) { val value = node.findAssignedValue() if (value is PyCallExpression && value.callee != null && PyTypedDictTypeProvider.isTypedDict(value.callee!!, myTypeEvalContext)) { - val typedDictName = value.getArgument(0, TYPED_DICT_NAME_PARAMETER, PyExpression::class.java) + val typedDictName = PyPsiUtils.flattenParens(value.arguments.firstOrNull()) if (typedDictName is PyStringLiteralExpression && node.name != typedDictName.stringValue) { registerProblem(typedDictName, PyPsiBundle.message("INSP.typeddict.first.argument.has.to.match.variable.name")) } @@ -89,24 +87,26 @@ class PyTypedDictInspection : PyInspection() { } } } - else if (node.callExpression != null) { + else { val callExpression = node.callExpression - val callee = callExpression!!.callee - if (callee != null && PyTypedDictTypeProvider.isTypedDict(callee, myTypeEvalContext)) { - val fields = callExpression.getArgument(1, TYPED_DICT_FIELDS_PARAMETER, PyExpression::class.java) - if (fields !is PyDictLiteralExpression) { - return - } + if (callExpression != null) { + val callee = callExpression.callee + if (callee != null && PyTypedDictTypeProvider.isTypedDict(callee, myTypeEvalContext)) { + val fields = PyPsiUtils.flattenParens(callExpression.arguments.getOrNull(1)) + if (fields !is PyDictLiteralExpression) { + return + } - fields.elements.forEach { - if (it !is PyKeyValueExpression) return + fields.elements.forEach { + if (it !is PyKeyValueExpression) return - checkValueIsAType(it.value, it.value?.text) - } + checkValueIsAType(it.value, it.value?.text) + } - val totalityArgument = callExpression.getKeywordArgument(TYPED_DICT_TOTAL_PARAMETER) - if (totalityArgument != null) { - checkValidTotality(totalityArgument) + val totalityArgument = callExpression.getKeywordArgument(TYPED_DICT_TOTAL_PARAMETER) + if (totalityArgument != null) { + checkValidTotality(totalityArgument) + } } } } diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyElementGeneratorImpl.java b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyElementGeneratorImpl.java index affeab9eae3c..b44a5ed0523d 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyElementGeneratorImpl.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyElementGeneratorImpl.java @@ -384,6 +384,11 @@ public final class PyElementGeneratorImpl extends PyElementGenerator { return createFromText(LanguageLevel.PYTHON30, PySingleStarParameter.class, "def foo(*): pass", new int[]{0, 3, 1}); } + @Override + public @NotNull PySlashParameter createSlashParameter() { + return createFromText(LanguageLevel.PYTHON30, PySlashParameter.class, "def foo(/): pass", new int[]{0, 3, 1}); + } + private static class CommasOnly extends NotNullPredicate { @Override protected boolean applyNotNull(@NotNull final LeafPsiElement input) { diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/stubs/PyTypedDictStubImpl.kt b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/stubs/PyTypedDictStubImpl.kt index 5096e81cce64..2e61e39a4257 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/stubs/PyTypedDictStubImpl.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/stubs/PyTypedDictStubImpl.kt @@ -11,8 +11,6 @@ import com.jetbrains.python.psi.impl.PyPsiUtils import com.jetbrains.python.psi.resolve.PyResolveUtil import com.jetbrains.python.psi.stubs.PyTypedDictFieldStub import com.jetbrains.python.psi.stubs.PyTypedDictStub -import com.jetbrains.python.psi.types.PyTypedDictType.Companion.TYPED_DICT_FIELDS_PARAMETER -import com.jetbrains.python.psi.types.PyTypedDictType.Companion.TYPED_DICT_NAME_PARAMETER import com.jetbrains.python.psi.types.PyTypedDictType.Companion.TYPED_DICT_TOTAL_PARAMETER import java.io.IOException import java.util.* @@ -58,24 +56,18 @@ class PyTypedDictStubImpl private constructor(private val myCalleeName: Qualifie fun create(expression: PyCallExpression): PyTypedDictStub? { val calleeReference = expression.callee as? PyReferenceExpression ?: return null + val calleeName = getCalleeName(calleeReference) ?: return null - val calleeName = getCalleeName(calleeReference) + val arguments = expression.arguments + val typeName = PyResolveUtil.resolveStrArgument(arguments.getOrNull(0)) ?: return null - if (calleeName != null) { - val name = PyResolveUtil.resolveStrArgument(expression, 0, TYPED_DICT_NAME_PARAMETER) ?: return null + val fieldsArg = PyPsiUtils.flattenParens(arguments.getOrNull(1)) + val fields = if (fieldsArg is PyDictLiteralExpression) getTypedDictFieldsFromDictLiteral(fieldsArg) else emptyList() - val fieldsArgument = expression.getArgument(1, TYPED_DICT_FIELDS_PARAMETER, PyDictLiteralExpression::class.java) ?: return null - - val fields = getTypingTDFieldsFromIterable(fieldsArgument) - if (fields != null) { - return PyTypedDictStubImpl(calleeName, - name, - fields, - PyEvaluator.evaluateAsBoolean(expression.getKeywordArgument(TYPED_DICT_TOTAL_PARAMETER), true)) - } - } - - return null + return PyTypedDictStubImpl(calleeName, + typeName, + fields, + PyEvaluator.evaluateAsBoolean(expression.getKeywordArgument(TYPED_DICT_TOTAL_PARAMETER), true)) } @Throws(IOException::class) @@ -120,20 +112,14 @@ class PyTypedDictStubImpl private constructor(private val myCalleeName: Qualifie return fields } - private fun getTypingTDFieldsFromIterable(fields: PySequenceExpression): List? { - val result = ArrayList() - - fields.elements.forEach { - if (it !is PyKeyValueExpression) return null - - val name: PyExpression = it.key - val type: PyExpression? = it.value - - if (name !is PyStringLiteralExpression) return null - - result.add(PyTypedDictFieldStub(name.stringValue, type?.text, true)) + private fun getTypedDictFieldsFromDictLiteral(expression: PyDictLiteralExpression): List { + val result = mutableListOf() + expression.elements.forEach { + val key = it.key + if (key is PyStringLiteralExpression) { + result.add(PyTypedDictFieldStub(key.stringValue, it.value?.text, true)) + } } - return result } } diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/resolve/PyResolveUtil.java b/python/python-psi-impl/src/com/jetbrains/python/psi/resolve/PyResolveUtil.java index b6d09fe2c95f..a3892b4d5530 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/resolve/PyResolveUtil.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/resolve/PyResolveUtil.java @@ -365,7 +365,13 @@ public final class PyResolveUtil { // Point = namedtuple(typename="Point", ...) - final PyExpression expression = PyPsiUtils.flattenParens(callExpression.getArgument(index, keyword, PyExpression.class)); + PyExpression argument = callExpression.getArgument(index, keyword, PyExpression.class); + return resolveStrArgument(argument); + } + + @Nullable + public static String resolveStrArgument(@Nullable PyExpression argument) { + final PyExpression expression = PyPsiUtils.flattenParens(argument); if (expression instanceof PyReferenceExpression) { return PyPsiUtils.strValue(fullResolveLocally((PyReferenceExpression)expression)); diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypedDictType.kt b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypedDictType.kt index f4589e9a7220..007801779d5e 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypedDictType.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypedDictType.kt @@ -143,9 +143,7 @@ class PyTypedDictType @JvmOverloads constructor(private val name: String, companion object { - const val TYPED_DICT_NAME_PARAMETER = "name" - const val TYPED_DICT_FIELDS_PARAMETER = "fields" - const val TYPED_DICT_TOTAL_PARAMETER = "total" + const val TYPED_DICT_TOTAL_PARAMETER: String = "total" fun createFromKeysToValueTypes(anchor: PsiElement, keysToValueTypes: Map>): PyTypedDictType? { diff --git a/python/testData/inspections/PyArgumentListInspection/typedDictAlternativeSyntaxDefinition.py b/python/testData/inspections/PyArgumentListInspection/typedDictAlternativeSyntaxDefinition.py index fcb8e48f2510..82c1230d4964 100644 --- a/python/testData/inspections/PyArgumentListInspection/typedDictAlternativeSyntaxDefinition.py +++ b/python/testData/inspections/PyArgumentListInspection/typedDictAlternativeSyntaxDefinition.py @@ -3,5 +3,8 @@ from typing import TypedDict Movie = TypedDict('Movie', {'name': str, 'year': int}, total=False) Movie2 = TypedDict('Movie2', {'name': str, 'year': int}) -Movie3 = TypedDict() -Movie4 = TypedDict(total=False, fields={}, name='Movie4') +Movie3 = TypedDict() +Movie4 = TypedDict('Movie4') +Movie5 = TypedDict(typename='Movie5', fields={}) +Movie6 = TypedDict('Movie6', {}, False) +Movie7 = TypedDict('Movie7', {}, unknown_param=False) diff --git a/python/testData/stubs/TypedDictFieldsKeyword.py b/python/testData/stubs/TypedDictFieldsKeyword.py deleted file mode 100644 index d00ad72d9802..000000000000 --- a/python/testData/stubs/TypedDictFieldsKeyword.py +++ /dev/null @@ -1,3 +0,0 @@ -from typing import TypedDict - -td = TypedDict("name", total=False, fields={"x": str, "y": int}) \ No newline at end of file diff --git a/python/testData/stubs/TypedDictNameKeyword.py b/python/testData/stubs/TypedDictNameKeyword.py deleted file mode 100644 index 7dc8b2004e1a..000000000000 --- a/python/testData/stubs/TypedDictNameKeyword.py +++ /dev/null @@ -1,3 +0,0 @@ -from typing import TypedDict - -td = TypedDict(fields={"x": str, "y": int}, total=False, name="name") \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/PyParameterInfoTest.java b/python/testSrc/com/jetbrains/python/PyParameterInfoTest.java index 0d51ec42af68..b40c1b2713bb 100644 --- a/python/testSrc/com/jetbrains/python/PyParameterInfoTest.java +++ b/python/testSrc/com/jetbrains/python/PyParameterInfoTest.java @@ -1035,8 +1035,8 @@ public class PyParameterInfoTest extends LightMarkedTestCase { public void testDefiningTypedDictTypeAlternativeSyntax() { final Map test = loadTest(1); - feignCtrlP(test.get("").getTextOffset()).check("name: str, fields: dict[str, Any], total: bool = True", - new String[]{"name: str, "}, + feignCtrlP(test.get("").getTextOffset()).check("typename: str, fields: dict[str, type], *, /, total: bool = True", + new String[]{"typename: str, "}, ArrayUtilRt.EMPTY_STRING_ARRAY); } diff --git a/python/testSrc/com/jetbrains/python/PyStubsTest.java b/python/testSrc/com/jetbrains/python/PyStubsTest.java index 4564f98f4a44..c1be306a2bff 100644 --- a/python/testSrc/com/jetbrains/python/PyStubsTest.java +++ b/python/testSrc/com/jetbrains/python/PyStubsTest.java @@ -1083,16 +1083,6 @@ public class PyStubsTest extends PyTestCase { doTestTypingTypedDictArguments(); } - // PY-36008 - public void testTypedDictNameKeyword() { - doTestTypingTypedDictArguments(); - } - - // PY-36008 - public void testTypedDictFieldsKeyword() { - doTestTypingTypedDictArguments(); - } - // PY-41305 public void testDecoratorQualifiedNames() { final PyFile file = getTestFile(); diff --git a/python/testSrc/com/jetbrains/python/inspections/PyTypeCheckerInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyTypeCheckerInspectionTest.java index bc3965db9eea..1d298168c805 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyTypeCheckerInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyTypeCheckerInspectionTest.java @@ -1001,6 +1001,11 @@ public class PyTypeCheckerInspectionTest extends PyInspectionTestCase { Movie = TypedDict('Movie', {'name': str, 'year': int}, total=False) movie = {'name': 'Blade Runner', 'director': 'Ridley Scott'} # type: Movie """); + doTestByText(""" + from typing import TypedDict + BadTD = TypedDict('BadTD', unknown_param=True) + td = {'v': 1} # type: BadTD + """); } // PY-36008 @@ -1106,8 +1111,9 @@ public class PyTypeCheckerInspectionTest extends PyInspectionTestCase { LanguageLevel.PYTHON36, () -> doTestByText(""" from typing import TypedDict - Movie = TypedDict(3, [1, 2, 3]) - Movie = TypedDict('Movie', {})""")); + Movie = TypedDict(3, [1, 2, 3]) + Movie = TypedDict('Movie', {}) + Movie = TypedDict('Movie', {'name': str})""")); } // PY-36008