mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
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:
committed by
intellij-monorepo-bot
parent
0703c6413c
commit
49b3258737
@@ -157,4 +157,7 @@ public abstract class PyElementGenerator extends PyAstElementGenerator {
|
||||
|
||||
@NotNull
|
||||
public abstract PySingleStarParameter createSingleStarParameter();
|
||||
|
||||
@NotNull
|
||||
public abstract PySlashParameter createSlashParameter();
|
||||
}
|
||||
|
||||
+15
-12
@@ -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)
|
||||
}
|
||||
|
||||
+17
-17
@@ -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) {
|
||||
|
||||
+16
-30
@@ -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? {
|
||||
|
||||
+5
-2
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user