PY-53611 Add support of Required and NotRequired TypedDict type specifications (PEP-655)

Makes it possible to mark individual TypedDict keys as required or not required, covered in [PEP-655](https://peps.python.org/pep-0655/)

GitOrigin-RevId: 6567fd1009430e37f32924eb29ab8b4a1a17f315
This commit is contained in:
lada.gagina
2022-06-21 20:29:34 +00:00
committed by intellij-monorepo-bot
parent 9f85df2165
commit 6bdc3d4237
12 changed files with 321 additions and 68 deletions
@@ -1124,6 +1124,9 @@ 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.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
# PyTypeHintsInspection
INSP.NAME.type.hints=Invalid type hints definitions and usages
@@ -3,6 +3,7 @@ package com.jetbrains.python.codeInsight.typing
import com.intellij.openapi.util.Ref
import com.intellij.psi.PsiElement
import com.intellij.psi.impl.source.resolve.FileContextUtil
import com.jetbrains.python.PyCustomType
import com.jetbrains.python.PyNames
import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider.*
@@ -179,26 +180,66 @@ class PyTypedDictTypeProvider : PyTypeProviderBase() {
return TDFields(type.fields)
}
val fields = mutableListOf<PyTargetExpression>()
val fields = mutableListOf<Pair<PyExpression, Boolean?>>()
cls.processClassLevelDeclarations { element, _ ->
if (element is PyTargetExpression && element.annotationValue != null) {
fields.add(element)
if (element is PyTargetExpression) {
val stub = element.stub
if (context.maySwitchToAST(cls) || stub == null) {
if (element.annotation != null) {
fields.add(Pair(element, checkTypeSpecification(element.annotation!!.value, context)))
}
}
else {
if (stub.annotation != null) {
val annotation = PyUtil.createExpressionFromFragment(stub.annotation!!, cls)
fields.add(Pair(stub.psi, checkTypeSpecification(annotation, context)))
}
}
}
true
}
val totality = getTotality(cls)
val toTDFields = Collectors.toMap<PyTargetExpression, String, PyTypedDictType.FieldTypeAndTotality, TDFields>(
{ it.name },
{ field -> PyTypedDictType.FieldTypeAndTotality(field, context.getType(field), totality) },
val toTDFields = Collectors.toMap<Pair<PyExpression, Boolean?>, String, PyTypedDictType.FieldTypeAndTotality, TDFields>(
{ it.first.name },
{ field -> PyTypedDictType.FieldTypeAndTotality(field.first, context.getType(field.first), field.second ?: totality) },
{ _, v2 -> v2 },
{ TDFields() })
return fields.stream().collect(toTDFields)
}
private fun checkTypeSpecification(annotation: PyExpression?, context: TypeEvalContext): Boolean? {
if (annotation is PySubscriptionExpression) {
var operand = annotation.operand
if (operand is PyReferenceExpression) {
if (resolveToQualifiedNames(operand, context).any { name -> ANNOTATED == name || ANNOTATED_EXT == name }) {
val annotatedType = (annotation as? PySubscriptionExpression)?.indexExpression?.children?.get(0)
if (annotatedType is PySubscriptionExpression) {
operand = annotatedType.operand
}
}
return isRequired(operand, context)
}
}
return null
}
/**
* @return true - if value type is Required, false - if it is NotRequired, and null if it does not have any type specification
*/
fun isRequired(expression: PyExpression, context: TypeEvalContext): Boolean? {
val resolvedNames = resolveToQualifiedNames(expression, context)
if (resolvedNames.any { name -> REQUIRED == name || REQUIRED_EXT == name }) {
return true
}
else if (resolvedNames.any { name -> NOT_REQUIRED == name || NOT_REQUIRED_EXT == name }) {
return false
}
return null
}
private fun getTotality(cls: PyClass): Boolean {
return if (cls.stub != null) {
"total=False" !in cls.stub.superClassesText
@@ -286,55 +327,27 @@ class PyTypedDictTypeProvider : PyTypeProviderBase() {
total: Boolean): PyTypedDictType.FieldTypeAndTotality {
if (type == null) return PyTypedDictType.FieldTypeAndTotality(null, null)
val pyType = Ref.deref(getStringBasedType(type, anchor, context))
return PyTypedDictType.FieldTypeAndTotality(null, pyType, total)
val valueTypeWithTotality = getStringBasedTypeForTypedDict(type, anchor, context)
if (valueTypeWithTotality == null) return PyTypedDictType.FieldTypeAndTotality(null, null)
val pyType = Ref.deref(valueTypeWithTotality.first)
val requiredField = valueTypeWithTotality.second
return PyTypedDictType.FieldTypeAndTotality(null, pyType, requiredField ?: total)
}
/**
* If [expected] type is `typing.TypedDict[...]`,
* then tries to infer `typing.TypedDict[...]` for [expression],
* otherwise returns type inferred by [context].
*/
fun promoteToTypedDict(expression: PyExpression, expected: PyType?, context: TypeEvalContext): PyType? {
if (expected is PyTypedDictType) {
return newInstance(expression, context) ?: context.getType(expression)
}
else {
return context.getType(expression)
private fun getStringBasedTypeForTypedDict(contents: String,
anchor: PsiElement,
context: TypeEvalContext): Pair<Ref<PyType?>?, Boolean?>? {
val file = FileContextUtil.getContextFile(anchor) ?: return null
val expr = PyUtil.createExpressionFromFragment(contents, file)
var required: Boolean? = null
if (expr is PySubscriptionExpression) {
required = isRequired(expr.operand, context)
}
return if (expr != null) Pair(getType(expr, context), required) else null
}
/**
* Tries to construct TypedDict type for a value that could be considered as TypedDict and downcasted to `typing.TypedDict[...]` type.
*/
private fun newInstance(expression: PyExpression, context: TypeEvalContext): PyType? {
return when (expression) {
is PyTupleExpression -> {
val elements = expression.elements
val classes = elements.mapNotNull { toTypedDictType(it, context) }
if (elements.size == classes.size) PyUnionType.union(classes) else null
}
else -> toTypedDictType(expression, context)
}
}
private fun toTypedDictType(expression: PyExpression, context: TypeEvalContext): PyType? {
if (expression is PyDictLiteralExpression) {
val fields = getTypingTDFieldsFromDictLiteral(expression, context)
if (fields != null) {
val dictClass = PyBuiltinCache.getInstance(expression).dictType?.pyClass
if (dictClass == null) return null
return PyTypedDictType("TypedDict", fields, true, dictClass,
PyTypedDictType.DefinitionLevel.INSTANCE,
emptyList())
}
}
else if (expression is PyCallExpression) {
val typedDictType = inferTypedDictFromCallExpression(expression, context)
if (typedDictType != null) return typedDictType
}
return null
}
fun inferTypedDictFromCallExpression(callExpression: PyCallExpression, context: TypeEvalContext): PyTypedDictType? {
val resolvedQualifiedNames = if (callExpression.callee != null) resolveToQualifiedNames(callExpression.callee!!, context)
@@ -355,21 +368,6 @@ class PyTypedDictTypeProvider : PyTypeProviderBase() {
return null
}
private fun getTypingTDFieldsFromDictLiteral(dictLiteral: PyDictLiteralExpression, context: TypeEvalContext): TDFields? {
val fields = LinkedHashMap<String, PyExpression?>()
dictLiteral.elements.forEach {
val name = it.key
val value = it.value
if (name !is PyStringLiteralExpression) return null
fields[name.stringValue] = value
}
return typedDictFieldsFromKeysAndValues(fields, context)
}
private fun getTypingTDFieldsFromDictKeywordArguments(keywordArguments: Array<PyExpression>, context: TypeEvalContext): TDFields? {
val fields = LinkedHashMap<String, PyExpression?>()
@@ -103,6 +103,10 @@ public class PyTypingTypeProvider extends PyTypeProviderBase {
public static final String TYPE_ALIAS_EXT = "typing_extensions.TypeAlias";
private static final String SPECIAL_FORM = "typing._SpecialForm";
private static final String SPECIAL_FORM_EXT = "typing_extensions._SpecialForm";
public static final String REQUIRED = "typing.Required";
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";
private static final String PY2_FILE_TYPE = "typing.BinaryIO";
private static final String PY3_BINARY_FILE_TYPE = "typing.BinaryIO";
@@ -143,10 +147,10 @@ public class PyTypingTypeProvider extends PyTypeProviderBase {
public static final ImmutableSet<String> GENERIC_CLASSES = ImmutableSet.<String>builder()
// special forms
.add(TUPLE, GENERIC, PROTOCOL, CALLABLE, TYPE, CLASS_VAR, FINAL, LITERAL, ANNOTATED)
.add(TUPLE, GENERIC, PROTOCOL, CALLABLE, TYPE, CLASS_VAR, FINAL, LITERAL, ANNOTATED, REQUIRED, NOT_REQUIRED)
// type aliases
.add(UNION, OPTIONAL, LIST, DICT, DEFAULT_DICT, ORDERED_DICT, SET, FROZEN_SET, COUNTER, DEQUE, CHAIN_MAP)
.add(PROTOCOL_EXT, FINAL_EXT, LITERAL_EXT, ANNOTATED_EXT)
.add(PROTOCOL_EXT, FINAL_EXT, LITERAL_EXT, ANNOTATED_EXT, REQUIRED_EXT, NOT_REQUIRED_EXT)
.build();
/**
@@ -186,6 +190,8 @@ public class PyTypingTypeProvider extends PyTypeProviderBase {
.add(TYPED_DICT, TYPED_DICT_EXT)
.add(ANNOTATED, ANNOTATED_EXT)
.add(TYPE_ALIAS, TYPE_ALIAS_EXT)
.add(REQUIRED, REQUIRED_EXT)
.add(NOT_REQUIRED, NOT_REQUIRED_EXT)
.build();
@Nullable
@@ -861,6 +867,10 @@ public class PyTypingTypeProvider extends PyTypeProviderBase {
if (annotatedType != null) {
return annotatedType;
}
final Ref<PyType> requiredOrNotRequiredType = getRequiredOrNotRequiredType(resolved, context);
if (requiredOrNotRequiredType != null) {
return requiredOrNotRequiredType;
}
final Ref<PyType> literalType = getLiteralType(resolved, context);
if (literalType != null) {
return literalType;
@@ -1111,6 +1121,26 @@ public class PyTypingTypeProvider extends PyTypeProviderBase {
return null;
}
@Nullable
private static Ref<PyType> getRequiredOrNotRequiredType(@NotNull PsiElement resolved, @NotNull Context context) {
if (resolved instanceof PySubscriptionExpression) {
final PySubscriptionExpression subscriptionExpr = (PySubscriptionExpression)resolved;
final PyExpression operand = subscriptionExpr.getOperand();
Collection<String> resolvedNames = resolveToQualifiedNames(operand, context.getTypeContext());
if (resolvedNames.stream().anyMatch(name -> REQUIRED.equals(name) || REQUIRED_EXT.equals(name) ||
NOT_REQUIRED.equals(name) || NOT_REQUIRED_EXT.equals(name))) {
final PyExpression indexExpr = subscriptionExpr.getIndexExpression();
final PyExpression type = indexExpr instanceof PyTupleExpression ? ((PyTupleExpression)indexExpr).getElements()[0] : indexExpr;
if (type != null) {
return getType(type, context);
}
}
}
return null;
}
@Nullable
private static Ref<PyType> getFinalType(@NotNull PsiElement resolved, @NotNull Context context) {
if (resolved instanceof PySubscriptionExpression) {
@@ -4,10 +4,12 @@ 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
import com.intellij.psi.PsiNameIdentifierOwner
import com.intellij.psi.util.PsiTreeUtil
import com.jetbrains.python.PyNames
import com.jetbrains.python.PyPsiBundle
import com.jetbrains.python.codeInsight.typing.PyTypedDictTypeProvider
@@ -21,6 +23,12 @@ 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,
@@ -254,6 +262,50 @@ class PyTypedDictInspection : PyInspection() {
}
}
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 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))
}
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))
}
}
}
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))
}
}
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))
}
}
}
}
private fun getIndexExpressionValueOptions(indexExpression: PyExpression?): List<String>? {
if (indexExpression == null) return null
val indexExprValue = PyEvaluator.evaluate(indexExpression, String::class.java)
@@ -283,6 +335,19 @@ class PyTypedDictInspection : PyInspection() {
registerProblem(expression, PyPsiBundle.message("INSP.typeddict.value.must.be.type"), ProblemHighlightType.WEAK_WARNING)
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"))
}
}
}
return
}
val type = Ref.deref(PyTypingTypeProvider.getStringBasedType(strType, expression, myTypeEvalContext))
if (type == null && !PyTypingTypeProvider.resolveToQualifiedNames(expression, myTypeEvalContext).any { qualifiedName ->
PyTypingTypeProvider.ANY == qualifiedName
@@ -0,0 +1,27 @@
from typing_extensions import TypedDict, Required, NotRequired
class _MovieBase0(TypedDict):
title: str
class Movie0(_MovieBase0, total=False):
year: int
class Movie1(TypedDict):
title: Required[str]
year: NotRequired[int]
class Movie2(TypedDict):
title: NotRequired[str]
year: NotRequired[int]
def f(movie: Movie0):
...
f(Movie1(title="Jaws"))
f(<warning descr="Expected type 'Movie0', got 'Movie2' instead">Movie2(title="Jaws")</warning>)
@@ -0,0 +1,6 @@
from required import A, AlternativeSyntax
a: A = <warning descr="TypedDict 'A' has missing keys: 'x', 'y'">{}</warning>
a1: A = {'x': 42, 'y': 42}
a2: AlternativeSyntax = {'y': <warning descr="Expected type 'int', got 'str' instead">"str"</warning>}
@@ -0,0 +1,9 @@
from typing_extensions import TypedDict, Annotated, Required, NotRequired
class A(TypedDict, total=False):
x: Required[int]
y: Annotated[Required[int], 'Some constraint']
AlternativeSyntax = TypedDict("AlternativeSyntax", {'x': NotRequired[int], 'y': Required[Annotated[int, 'Some constraint']]})
@@ -0,0 +1,15 @@
from typing_extensions import TypedDict, Required, NotRequired
class A(TypedDict):
x: int
y: NotRequired[int]
class B(TypedDict, total=False):
x: Required[int]
y: int
a = A(x=<arg1>)
b = B(x=<arg2>)
@@ -1016,6 +1016,18 @@ public class PyParameterInfoTest extends LightMarkedTestCase {
new String[]{"self: CallableTest, "});
}
// PY-53611
public void testTypedDictWithRequiredAndNotRequiredKeys() {
final Map<String, PsiElement> test = loadTest(2);
feignCtrlP(test.get("<arg1>").getTextOffset()).check("*, x: int, y: int = ...",
new String[]{"x: int, "},
ArrayUtilRt.EMPTY_STRING_ARRAY);
feignCtrlP(test.get("<arg2>").getTextOffset()).check("*, x: int, y: int = ...",
new String[]{"x: int, "},
ArrayUtilRt.EMPTY_STRING_ARRAY);
}
@NotNull
private Collector feignCtrlP(int offset) {
return feignCtrlP(offset, myFixture.getFile());
@@ -39,4 +39,17 @@ public class Py3ArgumentListInspectionTest extends PyInspectionTestCase {
"f = changes_return_type_to_str(returns_int)\n" +
"res2 = f(a=\"A\", b=True)");
}
// PY-53611
public void testTypedDictWithRequiredAndNotRequiredKeys() {
doTestByText("from typing_extensions import TypedDict, Required, NotRequired\n" +
"class A(TypedDict):\n" +
" x: int\n" +
" y: NotRequired[int]\n" +
"class B(TypedDict, total=False):\n" +
" x: Required[int]\n" +
" y: int\n" +
"a = A(<warning descr=\"Parameter 'x' unfilled\">)</warning>\n" +
"b = B(<warning descr=\"Parameter 'x' unfilled\">)</warning>");
}
}
@@ -2,6 +2,7 @@
package com.jetbrains.python.inspections;
import com.jetbrains.python.fixtures.PyInspectionTestCase;
import com.jetbrains.python.psi.LanguageLevel;
import org.jetbrains.annotations.NotNull;
/**
@@ -868,4 +869,52 @@ public class Py3TypeCheckerInspectionTest extends PyInspectionTestCase {
public void testTypedDictInReturnType() {
doTest();
}
// PY-53611
public void testTypedDictRequiredNotRequiredKeys() {
runWithLanguageLevel(LanguageLevel.getLatest(),
() -> doTestByText("from typing import TypedDict\n" +
"from typing_extensions import Required, NotRequired\n" +
"class WithTotalFalse(TypedDict, total=False):\n" +
" x: Required[int]\n" +
"class WithTotalTrue(TypedDict, total=True):\n" +
" x: NotRequired[int]\n" +
"class WithoutTotal(TypedDict):\n" +
" x: NotRequired[int]\n" +
"class WithoutTotalWithExplicitRequired(TypedDict):\n" +
" x: Required[int]\n" +
" y: NotRequired[int]\n" +
"AlternativeSyntax = TypedDict(\"AlternativeSyntax\", {'x': NotRequired[int]})\n" +
"with_total_false: WithTotalFalse = <warning descr=\"TypedDict 'WithTotalFalse' has missing key: 'x'\">{}</warning>\n" +
"with_total_true: WithTotalTrue = {}\n" +
"without_total: WithoutTotal = {}\n" +
"without_total_with_explicit_required: WithoutTotalWithExplicitRequired = <warning descr=\"TypedDict 'WithoutTotalWithExplicitRequired' has missing key: 'x'\">{}</warning>\n" +
"alternative_syntax: AlternativeSyntax = {}\n"));
}
// PY-53611
public void testTypedDictRequiredNotRequiredEquivalence() {
runWithLanguageLevel(LanguageLevel.getLatest(), this::doTest);
}
// PY-53611
public void testTypedDictRequiredNotRequiredMixedWithAnnotated() {
runWithLanguageLevel(LanguageLevel.getLatest(),
() -> doTestByText("from typing_extensions import TypedDict, Required, NotRequired, Annotated\n" +
"class A(TypedDict):\n" +
" x: Annotated[NotRequired[int], 'Some constraint']\n" +
"def f(a: A):\n" +
" pass\n" +
"f({})\n" +
"class B(TypedDict, total=False):\n" +
" x: Annotated[Required[int], 'Some constraint']\n" +
"def g(b: B):\n" +
" pass\n" +
"g(<warning descr=\"TypedDict 'B' has missing key: 'x'\">{}</warning>)\n"));
}
// PY-53611
public void testTypingRequiredTypeSpecificationsMultiFile() {
doMultiFileTest();
}
}
@@ -305,6 +305,32 @@ public class PyTypedDictInspectionTest extends PyInspectionTestCase {
" a: int | str");
}
// PY-53611
public void testRequiredOutsideTypedDictItems() {
doTestByText("from typing_extensions import Required, NotRequired\n" +
"x: <warning descr=\"'Required' can be used only in a TypedDict definition\">Required</warning>[int]\n" +
"y = print(<warning descr=\"'NotRequired' can be used only in a TypedDict definition\">NotRequired</warning>[int])\n" +
"class B:\n" +
" a: <warning descr=\"'Required' can be used only in a TypedDict definition\">Required</warning>[int]");
}
// PY-53611
public void testRequiredNotRequiredAtTheSameTime() {
doTestByText("from typing_extensions import TypedDict, Required, NotRequired\n" +
"class A(TypedDict):\n" +
" x: <warning descr=\"Key cannot be required and not required at the same time\">Required[NotRequired[int]]</warning>\n" +
" y: Required[int]\n" +
" z: NotRequired[int]\n" +
"A = TypedDict('A', {'x': <warning descr=\"Key cannot be required and not required at the same time\">Required[NotRequired[int]]</warning>, 'y': NotRequired[int]})");
}
// PY-53611
public void testRequiredWithMultipleParameters() {
doTestByText("from typing_extensions import TypedDict, Annotated, Required, NotRequired\n" +
"Alternative = TypedDict(\"Alternative\", {'x': Annotated[Required[int], \"constraint\"],\n" +
" 'y': NotRequired[<warning descr=\"'NotRequired' must have exactly one type argument\">Required[int], \"constraint\"</warning>]})");
}
@NotNull
@Override
protected Class<? extends PyInspection> getInspectionClass() {