mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-04-21 05:51:25 +07:00
PY-73099 Support PEP 705 – TypedDict: Read-only items
support for "ReadOnly" qualifier in chain with "Required" and "NotRequired" GitOrigin-RevId: 4ee6d82f7153d4b65217acb34acc71c4b6c20dc6
This commit is contained in:
committed by
intellij-monorepo-bot
parent
806e968ae3
commit
f4a04d15b6
@@ -1084,7 +1084,7 @@ INSP.typeddict.value.must.be.type=Value must be a type
|
||||
INSP.typeddict.total.value.must.be.true.or.false=Value of 'total' must be True or False
|
||||
INSP.typeddict.typeddict.cannot.have.key=TypedDict "{0}" cannot have key ''{1}''
|
||||
INSP.typeddict.cannot.add.non.string.key.to.typeddict=Cannot add a non-string key to TypedDict "{0}"
|
||||
INSP.typeddict.required.notrequired.cannot.be.used.outside.typeddict.definition=''{0}'' can be used only in a TypedDict definition
|
||||
INSP.typeddict.qualifiers.cannot.be.used.outside.typeddict.definition=''{0}'' can be used only in a TypedDict definition
|
||||
INSP.typeddict.cannot.be.required.and.not.required.at.the.same.time=Key cannot be required and not required at the same time
|
||||
INSP.typeddict.required.notrequired.must.have.exactly.one.type.argument=''{0}'' must have exactly one type argument
|
||||
|
||||
|
||||
@@ -116,6 +116,11 @@ public final class PyTypingTypeProvider extends PyTypeProviderWithCustomContext<
|
||||
public static final String REQUIRED_EXT = "typing_extensions.Required";
|
||||
public static final String NOT_REQUIRED = "typing.NotRequired";
|
||||
public static final String NOT_REQUIRED_EXT = "typing_extensions.NotRequired";
|
||||
public static final String READONLY = "typing.ReadOnly";
|
||||
public static final String READONLY_EXT = "typing_extensions.ReadOnly";
|
||||
|
||||
public static final Set<String> TYPE_DICT_QUALIFIERS = Set.of(REQUIRED, REQUIRED_EXT, NOT_REQUIRED, NOT_REQUIRED_EXT, READONLY, READONLY_EXT);
|
||||
|
||||
private static final String UNPACK = "typing.Unpack";
|
||||
private static final String UNPACK_EXT = "typing_extensions.Unpack";
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ package com.jetbrains.python.inspections
|
||||
import com.intellij.codeInspection.LocalInspectionToolSession
|
||||
import com.intellij.codeInspection.ProblemHighlightType
|
||||
import com.intellij.codeInspection.ProblemsHolder
|
||||
import com.intellij.openapi.util.NlsSafe
|
||||
import com.intellij.openapi.util.Ref
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.PsiElementVisitor
|
||||
@@ -23,12 +22,6 @@ import com.jetbrains.python.psi.types.PyTypedDictType.Companion.TYPED_DICT_FIELD
|
||||
import com.jetbrains.python.psi.types.PyTypedDictType.Companion.TYPED_DICT_NAME_PARAMETER
|
||||
import com.jetbrains.python.psi.types.PyTypedDictType.Companion.TYPED_DICT_TOTAL_PARAMETER
|
||||
|
||||
@NlsSafe
|
||||
private const val REQUIRED = "Required"
|
||||
|
||||
@NlsSafe
|
||||
private const val NOT_REQUIRED = "NotRequired"
|
||||
|
||||
class PyTypedDictInspection : PyInspection() {
|
||||
|
||||
override fun buildVisitor(holder: ProblemsHolder,
|
||||
@@ -272,45 +265,42 @@ class PyTypedDictInspection : PyInspection() {
|
||||
}
|
||||
}
|
||||
|
||||
fun isTypeDictQualifier(node: PyReferenceExpression): Boolean =
|
||||
PyTypingTypeProvider.resolveToQualifiedNames(node, myTypeEvalContext).any { PyTypingTypeProvider.TYPE_DICT_QUALIFIERS.contains(it) }
|
||||
|
||||
override fun visitPyReferenceExpression(node: PyReferenceExpression) {
|
||||
if (PsiTreeUtil.getParentOfType(node, PyImportStatementBase::class.java) == null) {
|
||||
val isRequired = PyTypingTypeProvider.resolveToQualifiedNames(node, myTypeEvalContext).any { qualifiedName ->
|
||||
PyTypingTypeProvider.REQUIRED == qualifiedName ||
|
||||
PyTypingTypeProvider.REQUIRED_EXT == qualifiedName
|
||||
}
|
||||
val isNotRequired = PyTypingTypeProvider.resolveToQualifiedNames(node, myTypeEvalContext).any { qualifiedName ->
|
||||
PyTypingTypeProvider.NOT_REQUIRED == qualifiedName ||
|
||||
PyTypingTypeProvider.NOT_REQUIRED_EXT == qualifiedName
|
||||
}
|
||||
if (isRequired || isNotRequired) {
|
||||
val isTypeDictQualifier = isTypeDictQualifier(node)
|
||||
if (isTypeDictQualifier) {
|
||||
val qualifierName = node.name
|
||||
val classParent = PsiTreeUtil.getParentOfType(node, PyClass::class.java)
|
||||
val callParent = PsiTreeUtil.getParentOfType(node, PyCallExpression::class.java)
|
||||
if (classParent == null) {
|
||||
if (callParent == null) {
|
||||
registerProblem(node, PyPsiBundle.message("INSP.typeddict.required.notrequired.cannot.be.used.outside.typeddict.definition",
|
||||
if (isRequired) REQUIRED else NOT_REQUIRED))
|
||||
registerProblem(node, PyPsiBundle.message("INSP.typeddict.qualifiers.cannot.be.used.outside.typeddict.definition",
|
||||
qualifierName))
|
||||
}
|
||||
else {
|
||||
if (callParent.callee != null &&
|
||||
PyTypingTypeProvider.resolveToQualifiedNames(callParent.callee!!, myTypeEvalContext).none { qualifiedName ->
|
||||
PyTypingTypeProvider.TYPED_DICT == qualifiedName || PyTypingTypeProvider.TYPED_DICT_EXT == qualifiedName
|
||||
}) {
|
||||
registerProblem(node, PyPsiBundle.message("INSP.typeddict.required.notrequired.cannot.be.used.outside.typeddict.definition",
|
||||
if (isRequired) REQUIRED else NOT_REQUIRED))
|
||||
registerProblem(node, PyPsiBundle.message("INSP.typeddict.qualifiers.cannot.be.used.outside.typeddict.definition",
|
||||
qualifierName))
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!PyTypedDictTypeProvider.isTypingTypedDictInheritor(classParent, myTypeEvalContext)) {
|
||||
registerProblem(node, PyPsiBundle.message("INSP.typeddict.required.notrequired.cannot.be.used.outside.typeddict.definition",
|
||||
if (isRequired) REQUIRED else NOT_REQUIRED))
|
||||
registerProblem(node, PyPsiBundle.message("INSP.typeddict.qualifiers.cannot.be.used.outside.typeddict.definition",
|
||||
qualifierName))
|
||||
}
|
||||
}
|
||||
|
||||
if (node.parent is PySubscriptionExpression && (node.parent as PySubscriptionExpression).indexExpression is PyTupleExpression) {
|
||||
registerProblem((node.parent as PySubscriptionExpression).indexExpression,
|
||||
PyPsiBundle.message("INSP.typeddict.required.notrequired.must.have.exactly.one.type.argument",
|
||||
if (isRequired) REQUIRED else NOT_REQUIRED))
|
||||
qualifierName))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -346,16 +336,24 @@ class PyTypedDictInspection : PyInspection() {
|
||||
return
|
||||
}
|
||||
if (expression is PySubscriptionExpression && expression.operand is PyReferenceExpression) {
|
||||
if (expression.indexExpression is PySubscriptionExpression) {
|
||||
val indexExpression = expression.indexExpression as PySubscriptionExpression
|
||||
if (indexExpression.operand is PyReferenceExpression) {
|
||||
val operandIsRequired = PyTypedDictTypeProvider.isRequired(expression.operand as PyReferenceExpression, myTypeEvalContext)
|
||||
val indexIsRequired = PyTypedDictTypeProvider.isRequired(indexExpression.operand as PyReferenceExpression, myTypeEvalContext)
|
||||
if (operandIsRequired != null && indexIsRequired != null && operandIsRequired.xor(indexIsRequired)) {
|
||||
registerProblem(expression, PyPsiBundle.message("INSP.typeddict.cannot.be.required.and.not.required.at.the.same.time"))
|
||||
var requiredPresented = false
|
||||
var notRequiredPresented = false
|
||||
expression.accept(object: PyRecursiveElementVisitor() {
|
||||
override fun visitPySubscriptionExpression(node: PySubscriptionExpression) {
|
||||
if (PyTypedDictTypeProvider.isRequired(node.operand, myTypeEvalContext) == true) {
|
||||
requiredPresented = true
|
||||
}
|
||||
if (PyTypedDictTypeProvider.isRequired(node.operand, myTypeEvalContext) == false) {
|
||||
notRequiredPresented = true
|
||||
}
|
||||
if (requiredPresented && notRequiredPresented) {
|
||||
registerProblem(expression, PyPsiBundle.message("INSP.typeddict.cannot.be.required.and.not.required.at.the.same.time"))
|
||||
return
|
||||
}
|
||||
super.visitPySubscriptionExpression(node)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
val type = if (expression is PyReferenceExpression) {
|
||||
|
||||
@@ -354,12 +354,20 @@ public class PyTypedDictInspectionTest extends PyInspectionTestCase {
|
||||
A = TypedDict('A', {'x': <warning descr="Key cannot be required and not required at the same time">Required[NotRequired[int]]</warning>, 'y': NotRequired[int]})""");
|
||||
}
|
||||
|
||||
public void testRequiredNotRequiredWithReadOnly() {
|
||||
doTestByText("""
|
||||
from typing_extensions import TypedDict, Required, NotRequired, ReadOnly
|
||||
class A(TypedDict):
|
||||
x: <warning descr="Key cannot be required and not required at the same time">Required[ReadOnly[NotRequired[int]]]</warning>
|
||||
""");
|
||||
}
|
||||
|
||||
// PY-53611
|
||||
public void testRequiredWithMultipleParameters() {
|
||||
doTestByText("""
|
||||
from typing_extensions import TypedDict, Annotated, Required, NotRequired
|
||||
Alternative = TypedDict("Alternative", {'x': Annotated[Required[int], "constraint"],
|
||||
'y': NotRequired[<warning descr="'NotRequired' must have exactly one type argument">Required[int], "constraint"</warning>]})""");
|
||||
'y': NotRequired[<warning descr="'NotRequired' must have exactly one type argument">int, "constraint"</warning>]})""");
|
||||
}
|
||||
|
||||
// PY-55092
|
||||
|
||||
Reference in New Issue
Block a user