mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
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:
committed by
intellij-monorepo-bot
parent
9f85df2165
commit
6bdc3d4237
@@ -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
|
||||
|
||||
+64
-66
@@ -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?>()
|
||||
|
||||
|
||||
+32
-2
@@ -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
|
||||
|
||||
+27
@@ -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>)
|
||||
+6
@@ -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>}
|
||||
+9
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user