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
This commit is contained in:
Petr
2024-12-19 18:50:44 +00:00
committed by intellij-monorepo-bot
parent 0703c6413c
commit 49b3258737
13 changed files with 79 additions and 85 deletions
@@ -157,4 +157,7 @@ public abstract class PyElementGenerator extends PyAstElementGenerator {
@NotNull
public abstract PySingleStarParameter createSingleStarParameter();
@NotNull
public abstract PySlashParameter createSlashParameter();
}
@@ -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<PyCallableParameter>()
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)
}
@@ -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)
}
}
}
}
@@ -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<LeafPsiElement> {
@Override
protected boolean applyNotNull(@NotNull final LeafPsiElement input) {
@@ -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<PyTypedDictFieldStub>? {
val result = ArrayList<PyTypedDictFieldStub>()
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<PyTypedDictFieldStub> {
val result = mutableListOf<PyTypedDictFieldStub>()
expression.elements.forEach {
val key = it.key
if (key is PyStringLiteralExpression) {
result.add(PyTypedDictFieldStub(key.stringValue, it.value?.text, true))
}
}
return result
}
}
@@ -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));
@@ -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<String, Pair<PyExpression?, PyType?>>): PyTypedDictType? {
@@ -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(<warning descr="Parameter 'name' unfilled"><warning descr="Parameter 'fields' unfilled">)</warning></warning>
Movie4 = TypedDict(total=False, fields={}, name='Movie4')
Movie3 = TypedDict(<warning descr="Parameter 'fields' unfilled"><warning descr="Parameter 'typename' unfilled">)</warning></warning>
Movie4 = TypedDict('Movie4'<warning descr="Parameter 'fields' unfilled">)</warning>
Movie5 = TypedDict(<warning descr="Unexpected argument">typename='Movie5'</warning>, <warning descr="Unexpected argument">fields={}</warning><warning descr="Parameter 'typename' unfilled"><warning descr="Parameter 'fields' unfilled">)</warning></warning>
Movie6 = TypedDict('Movie6', {}, <warning descr="Unexpected argument">False</warning>)
Movie7 = TypedDict('Movie7', {}, <warning descr="Unexpected argument">unknown_param=False</warning>)
@@ -1,3 +0,0 @@
from typing import TypedDict
td = TypedDict("name", total=False, fields={"x": str, "y": int})
@@ -1,3 +0,0 @@
from typing import TypedDict
td = TypedDict(fields={"x": str, "y": int}, total=False, name="name")
@@ -1035,8 +1035,8 @@ public class PyParameterInfoTest extends LightMarkedTestCase {
public void testDefiningTypedDictTypeAlternativeSyntax() {
final Map<String, PsiElement> test = loadTest(1);
feignCtrlP(test.get("<arg1>").getTextOffset()).check("name: str, fields: dict[str, Any], total: bool = True",
new String[]{"name: str, "},
feignCtrlP(test.get("<arg1>").getTextOffset()).check("typename: str, fields: dict[str, type], *, /, total: bool = True",
new String[]{"typename: str, "},
ArrayUtilRt.EMPTY_STRING_ARRAY);
}
@@ -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();
@@ -1001,6 +1001,11 @@ public class PyTypeCheckerInspectionTest extends PyInspectionTestCase {
Movie = TypedDict('Movie', {'name': str, 'year': int}, total=False)
movie = {'name': 'Blade Runner', <warning descr="Extra key 'director' for TypedDict 'Movie'">'director': 'Ridley Scott'</warning>} # type: Movie
""");
doTestByText("""
from typing import TypedDict
BadTD = TypedDict('BadTD', unknown_param=True)
td = {<warning descr="Extra key 'v' for TypedDict 'BadTD'">'v': 1</warning>} # type: BadTD
""");
}
// PY-36008
@@ -1106,8 +1111,9 @@ public class PyTypeCheckerInspectionTest extends PyInspectionTestCase {
LanguageLevel.PYTHON36,
() -> doTestByText("""
from typing import TypedDict
Movie = TypedDict(<warning descr="Expected type 'str', got 'int' instead">3</warning>, <warning descr="Expected type 'Dict[str, Any]', got 'List[int]' instead">[1, 2, 3]</warning>)
Movie = TypedDict('Movie', {})"""));
Movie = TypedDict(<warning descr="Expected type 'str', got 'int' instead">3</warning>, <warning descr="Expected type 'Dict[str, type]', got 'List[int]' instead">[1, 2, 3]</warning>)
Movie = TypedDict('Movie', {})
Movie = TypedDict('Movie', {'name': str})"""));
}
// PY-36008