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:
Andrey Vokin
2024-08-13 05:36:58 +02:00
committed by intellij-monorepo-bot
parent 806e968ae3
commit f4a04d15b6
4 changed files with 44 additions and 33 deletions

View File

@@ -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

View File

@@ -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";

View File

@@ -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) {

View File

@@ -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